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

0001 /***************************************************************************
0002     Copyright (C) 2004-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 /*
0026  * We could use KCompactDisc now, but it's very buggy,
0027  * see https://bugs.kde.org/show_bug.cgi?id=183520
0028  *     https://bugs.kde.org/show_bug.cgi?id=183521
0029  *     https://lists.kde.org/?l=kde-multimedia&m=123397778013726&w=2
0030  */
0031 
0032 #include <config.h>
0033 
0034 #include "freedbimporter.h"
0035 #include "../collections/musiccollection.h"
0036 #include "../entry.h"
0037 #include "../fieldformat.h"
0038 #include "../utils/tellico_utils.h"
0039 #include "../utils/string_utils.h"
0040 #include "../utils/guiproxy.h"
0041 #include "../progressmanager.h"
0042 #include "../utils/cursorsaver.h"
0043 #include "../tellico_debug.h"
0044 
0045 #if defined HAVE_KCDDB
0046 #include <KCddb/Client>
0047 #elif defined HAVE_KCDDB
0048 #include <libkcddb/client.h>
0049 #endif
0050 
0051 #include <KComboBox>
0052 #include <KSharedConfig>
0053 #include <KConfigGroup>
0054 #include <KLocalizedString>
0055 
0056 #include <QInputDialog>
0057 #include <QFile>
0058 #include <QDir>
0059 #include <QLabel>
0060 #include <QGroupBox>
0061 #include <QRadioButton>
0062 #include <QButtonGroup>
0063 #include <QCheckBox>
0064 #include <QTextStream>
0065 #include <QVBoxLayout>
0066 #include <QTextCodec>
0067 #include <QApplication>
0068 
0069 using Tellico::Import::FreeDBImporter;
0070 
0071 FreeDBImporter::FreeDBImporter() : Tellico::Import::Importer()
0072     , m_widget(nullptr)
0073     , m_buttonGroup(nullptr)
0074     , m_radioCDROM(nullptr)
0075     , m_radioCache(nullptr)
0076     , m_driveCombo(nullptr)
0077     , m_cancelled(false) {
0078 }
0079 
0080 bool FreeDBImporter::canImport(int type) const {
0081   return type == Data::Collection::Album;
0082 }
0083 
0084 Tellico::Data::CollPtr FreeDBImporter::collection() {
0085   if(m_coll) {
0086     return m_coll;
0087   }
0088 
0089   m_cancelled = false;
0090   if(m_radioCDROM->isChecked()) {
0091     readCDROM();
0092   } else {
0093     readCache();
0094   }
0095   if(m_cancelled) {
0096     m_coll = Data::CollPtr();
0097   }
0098   return m_coll;
0099 }
0100 
0101 void FreeDBImporter::readCDROM() {
0102 #if defined (HAVE_OLD_KCDDB) || defined (HAVE_KCDDB)
0103   QString drivePath = m_driveCombo->currentText();
0104   if(drivePath.isEmpty()) {
0105     setStatusMessage(i18n("<qt>Tellico was unable to access the CD-ROM device - <i>%1</i>.</qt>", drivePath));
0106     myDebug() << "no drive!";
0107     return;
0108   }
0109 
0110   // now it's ok to add device to saved list
0111   m_driveCombo->addItem(drivePath);
0112   QStringList drives;
0113   for(int i = 0; i < m_driveCombo->count(); ++i) {
0114     if(drives.indexOf(m_driveCombo->itemText(i)) == -1) {
0115       drives += m_driveCombo->itemText(i);
0116     }
0117   }
0118 
0119   {
0120     KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("ImportOptions - FreeDB"));
0121     config.writeEntry("CD-ROM Devices", drives);
0122     config.writeEntry("Last Device", drivePath);
0123     config.writeEntry("Cache Files Only", false);
0124   }
0125 
0126   QByteArray drive = QFile::encodeName(drivePath);
0127   QList<uint> lengths;
0128   KCDDB::TrackOffsetList list;
0129 #if 0
0130   // a1107d0a - Kruder & Dorfmeister - The K&D Sessions - Disc One.
0131 /*  list
0132     << 150      // First track start.
0133     << 29462
0134     << 66983
0135     << 96785
0136     << 135628
0137     << 168676
0138     << 194147
0139     << 222158
0140     << 247076
0141     << 278203   // Last track start.
0142     << 316732;  // Disc end.
0143 */
0144 /*
0145   list
0146     << 150      // First track start.
0147     << 3296
0148     << 14437
0149     << 41279
0150     << 51362
0151     << 56253
0152     << 59755
0153     << 61324
0154     << 66059
0155     << 69073
0156     << 77790
0157     << 83214
0158     << 89726
0159     << 92078
0160     << 106325
0161     << 113117
0162     << 116040
0163     << 119877
0164     << 124377
0165     << 145466
0166     << 157583
0167     << 167208
0168     << 173486
0169     << 180120
0170     << 185279
0171     << 193270
0172     << 206451
0173     << 217303   // Last track start.
0174     << 224925;  // Disc end.
0175   list
0176     << 150
0177     << 106965
0178     << 127220
0179     << 151925
0180     << 176085
0181     << 234500;
0182 */
0183 #else
0184   list = offsetList(drive, lengths);
0185 #endif
0186 
0187   if(list.isEmpty()) {
0188     setStatusMessage(i18n("<qt>Tellico was unable to access the CD-ROM device - <i>%1</i>.</qt>", drivePath));
0189     return;
0190   }
0191 //  myDebug() << list;
0192 
0193   // the result info, could be multiple ones
0194   KCDDB::CDInfo info;
0195   KCDDB::Client client;
0196   client.setBlockingMode(true);
0197 
0198   KCDDB::Result r = client.lookup(list);
0199   const KCDDB::CDInfoList responseList = client.lookupResponse();
0200   // KCDDB sometimes doesn't return MultipleRecordFound properly, so check length of response, too
0201   if(r == KCDDB::MultipleRecordFound || responseList.count() > 1) {
0202     QStringList list;
0203     foreach(const KCDDB::CDInfo& info, responseList) {
0204       list.append(QStringLiteral("%1, %2, %3").arg(info.get(KCDDB::Artist).toString(),
0205                                                         info.get(KCDDB::Title).toString(),
0206                                                         info.get(KCDDB::Genre).toString()));
0207     }
0208 
0209     // switch back to pointer cursor
0210     GUI::CursorSaver cs(Qt::ArrowCursor);
0211     bool ok;
0212     QString res = QInputDialog::getItem(GUI::Proxy::widget(),
0213                                         i18n("Select CDDB Entry"),
0214                                         i18n("Select a CDDB entry:"),
0215                                         list, 0, false, &ok);
0216     if(ok) {
0217       int i = 0;
0218       foreach(const QString& listValue, list) {
0219         if(listValue == res) {
0220           break;
0221         }
0222         ++i;
0223       }
0224       if(i < responseList.size()) {
0225         info = responseList.at(i);
0226       }
0227     } else { // cancelled dialog
0228       m_cancelled = true;
0229     }
0230   } else if(r == KCDDB::Success && !responseList.isEmpty()) {
0231     info = responseList.first();
0232   } else {
0233     myDebug() << "no success! Return value = " << r;
0234     QString s;
0235     switch(r) {
0236       case KCDDB::NoRecordFound:
0237         s = i18n("<qt>No records were found to match the CD.</qt>");
0238         break;
0239       case KCDDB::ServerError:
0240         myDebug() << "Server Error";
0241         break;
0242       case KCDDB::HostNotFound:
0243         myDebug() << "Host Not Found";
0244         break;
0245       case KCDDB::NoResponse:
0246         myDebug() << "No Response";
0247         break;
0248       case KCDDB::UnknownError:
0249         myDebug() << "Unknown Error";
0250         break;
0251       default:
0252         break;
0253     }
0254     if(s.isEmpty()) {
0255       s = i18n("<qt>Tellico was unable to complete the CD lookup.</qt>");
0256     }
0257     setStatusMessage(s);
0258     return;
0259   }
0260 
0261   if(!info.isValid()) {
0262     // go ahead and try to read cd-text if we weren't cancelled
0263     // could be the case we don't have net access
0264     if(!m_cancelled) {
0265       readCDText(drive);
0266     }
0267     return;
0268   }
0269 
0270   m_coll = new Data::MusicCollection(true);
0271 
0272   Data::EntryPtr entry(new Data::Entry(m_coll));
0273   // obviously a CD
0274   entry->setField(QStringLiteral("medium"), i18n("Compact Disc"));
0275   entry->setField(QStringLiteral("title"),  info.get(KCDDB::Title).toString());
0276   entry->setField(QStringLiteral("artist"), info.get(KCDDB::Artist).toString());
0277   entry->setField(QStringLiteral("genre"),  info.get(KCDDB::Genre).toString());
0278   if(!info.get(KCDDB::Year).isNull()) {
0279     entry->setField(QStringLiteral("year"), info.get(KCDDB::Year).toString());
0280   }
0281   entry->setField(QStringLiteral("keyword"), info.get(KCDDB::Category).toString());
0282   QString extd = info.get(QStringLiteral("EXTD")).toString();
0283   extd.replace(QLatin1Char('\n'), QLatin1String("<br/>"));
0284   entry->setField(QStringLiteral("comments"), extd);
0285 
0286   QStringList trackList;
0287   trackList.reserve(info.numberOfTracks());
0288   for(int i = 0; i < info.numberOfTracks(); ++i) {
0289     QString s = info.track(i).get(KCDDB::Title).toString() + FieldFormat::columnDelimiterString();
0290     const QString trackArtist = info.track(i).get(KCDDB::Artist).toString().trimmed();
0291     s += trackArtist.isEmpty() ? info.get(KCDDB::Artist).toString() : trackArtist;
0292     if(i < lengths.count()) {
0293       s += FieldFormat::columnDelimiterString() + Tellico::minutes(lengths[i]);
0294     }
0295     trackList << s;
0296   }
0297   entry->setField(QStringLiteral("track"), trackList.join(FieldFormat::rowDelimiterString()));
0298 
0299   m_coll->addEntries(entry);
0300   readCDText(drive);
0301 #endif
0302 }
0303 
0304 void FreeDBImporter::readCache() {
0305 #if defined (HAVE_OLD_KCDDB) || defined (HAVE_KCDDB)
0306   {
0307     // remember the import options
0308     KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("ImportOptions - FreeDB"));
0309     config.writeEntry("Cache Files Only", true);
0310   }
0311 
0312   KCDDB::Config cfg;
0313   cfg.load();
0314 
0315   const QStringList cacheDirs = cfg.cacheLocations();
0316   QStringList dirs = cacheDirs;
0317   foreach(const QString& dirName, cacheDirs) {
0318     dirs += Tellico::findAllSubDirs(dirName);
0319   }
0320 
0321   // using a QMap is a lazy man's way of getting unique keys
0322   // the cddb info may be in multiple files, all with the same filename, the cddb id
0323   QMap<QString, QString> files;
0324   foreach(const QString& dirName, dirs) {
0325     if(dirName.isEmpty()) {
0326       continue;
0327     }
0328 
0329     QDir dir(dirName);
0330     dir.setFilter(QDir::Files | QDir::Readable | QDir::Hidden); // hidden since I want directory files
0331     const QStringList list = dir.entryList();
0332     foreach(const QString& listEntry, list) {
0333       files.insert(listEntry, dir.absoluteFilePath(listEntry));
0334     }
0335 //    qApp->processEvents(); // really needed ?
0336   }
0337 
0338   const QString title    = QStringLiteral("title");
0339   const QString artist   = QStringLiteral("artist");
0340   const QString year     = QStringLiteral("year");
0341   const QString genre    = QStringLiteral("genre");
0342   const QString medium   = QStringLiteral("medium");
0343   const QString keyword  = QStringLiteral("keyword");
0344   const QString track    = QStringLiteral("track");
0345   const QString comments = QStringLiteral("comments");
0346   int numFiles = files.count();
0347 
0348   if(numFiles == 0) {
0349     myDebug() << "no files found";
0350     return;
0351   }
0352 
0353   m_coll = new Data::MusicCollection(true);
0354 
0355   const uint stepSize = qMax(1, numFiles / 100);
0356   const bool showProgress = options() & ImportProgress;
0357 
0358   ProgressItem& item = ProgressManager::self()->newProgressItem(this, progressLabel(), true);
0359   item.setTotalSteps(numFiles);
0360   connect(&item, &Tellico::ProgressItem::signalCancelled, this, &FreeDBImporter::slotCancel);
0361   ProgressItem::Done done(this);
0362 
0363   uint step = 1;
0364 
0365   KCDDB::CDInfo info;
0366   for(QMap<QString, QString>::ConstIterator it = files.constBegin(); !m_cancelled && it != files.constEnd(); ++it, ++step) {
0367     // open file and read content
0368     QFileInfo fileinfo(it.value()); // skip files larger than 10 kB
0369     if(!fileinfo.exists() || !fileinfo.isReadable() || fileinfo.size() > 10*1024) {
0370       myDebug() << "skipping " << it.value();
0371       continue;
0372     }
0373     QFile file(it.value());
0374     if(!file.open(QIODevice::ReadOnly)) {
0375       continue;
0376     }
0377     QTextStream ts(&file);
0378     // libkcddb always writes the cache files in utf-8
0379     ts.setCodec(QTextCodec::codecForName("UTF-8"));
0380     QString cddbData = ts.readAll();
0381     file.close();
0382 
0383     if(cddbData.isEmpty() || !info.load(cddbData) || !info.isValid()) {
0384       myDebug() << "Error - CDDB record is not valid";
0385       myDebug() << "File = " << it.value();
0386       continue;
0387     }
0388 
0389     // create a new entry and set fields
0390     Data::EntryPtr entry(new Data::Entry(m_coll));
0391     // obviously a CD
0392     entry->setField(medium, i18n("Compact Disc"));
0393     entry->setField(title, info.get(KCDDB::Title).toString());
0394     entry->setField(artist, info.get(KCDDB::Artist).toString());
0395     entry->setField(genre, info.get(KCDDB::Genre).toString());
0396     if(!info.get(KCDDB::Year).isNull()) {
0397       entry->setField(year, info.get(KCDDB::Year).toString());
0398     }
0399     entry->setField(keyword, info.get(KCDDB::Category).toString());
0400     QString extd = info.get(QStringLiteral("EXTD")).toString();
0401     extd.replace(QLatin1Char('\n'), QLatin1String("<br/>"));
0402     entry->setField(comments, extd);
0403 
0404     // step through trackList
0405     QStringList trackList;
0406     trackList.reserve(info.numberOfTracks());
0407     for(int i = 0; i < info.numberOfTracks(); ++i) {
0408       trackList << info.track(i).get(KCDDB::Title).toString();
0409     }
0410     entry->setField(track, trackList.join(FieldFormat::rowDelimiterString()));
0411 
0412 #if 0
0413     // add CDDB info
0414     const QString br = QLatin1String("<br/>");
0415     QString comment;
0416     if(!info.extd.isEmpty()) {
0417       comment.append(info.extd + br);
0418     }
0419     if(!info.id.isEmpty()) {
0420       comment.append(QLatin1String("CDDB-ID: ") + info.id + br);
0421     }
0422     if(info.length > 0) {
0423       comment.append("Length: " + QString::number(info.length) + br);
0424     }
0425     if(info.revision > 0) {
0426       comment.append("Revision: " + QString::number(info.revision) + br);
0427     }
0428     entry->setField(comments, comment);
0429 #endif
0430 
0431     // add this entry to the music collection
0432     m_coll->addEntries(entry);
0433 
0434     if(showProgress && step%stepSize == 0) {
0435       ProgressManager::self()->setProgress(this, step);
0436       qApp->processEvents();
0437     }
0438   }
0439 #endif
0440 }
0441 
0442 #define SETFIELD(name,value) \
0443   if(entry->field(QStringLiteral(name)).isEmpty()) { \
0444     entry->setField(QStringLiteral(name), value); \
0445   }
0446 
0447 void FreeDBImporter::readCDText(const QByteArray& drive_) {
0448 #ifdef ENABLE_CDTEXT
0449   Data::EntryPtr entry;
0450   if(m_coll) {
0451     if(m_coll->entryCount() > 0) {
0452       entry = m_coll->entries().front();
0453     }
0454   } else {
0455     m_coll = new Data::MusicCollection(true);
0456   }
0457   if(!entry) {
0458     entry = new Data::Entry(m_coll);
0459     entry->setField(QStringLiteral("medium"), i18n("Compact Disc"));
0460     m_coll->addEntries(entry);
0461   }
0462 
0463   CDText cdtext = getCDText(drive_);
0464 /*
0465   myDebug() << "CDText - title: " << cdtext.title;
0466   myDebug() << "CDText - title: " << cdtext.artist;
0467   for(int i = 0; i < cdtext.trackTitles.size(); ++i) {
0468     myDebug() << i << "--" << cdtext.trackTitles[i] << " - " << cdtext.trackArtists[i];
0469   }
0470 */
0471 
0472   QString artist = cdtext.artist;
0473   SETFIELD("title",    cdtext.title);
0474   SETFIELD("artist",   artist);
0475   SETFIELD("comments", cdtext.message);
0476   QStringList tracks;
0477   tracks.reserve(cdtext.trackTitles.size());
0478   for(int i = 0; i < cdtext.trackTitles.size(); ++i) {
0479     tracks << cdtext.trackTitles[i] + FieldFormat::columnDelimiterString() + cdtext.trackArtists[i];
0480     if(artist.isEmpty()) {
0481       artist = cdtext.trackArtists[i];
0482     }
0483     if(!artist.isEmpty() && artist.toLower() != cdtext.trackArtists[i].toLower()) {
0484       artist = i18n("Various");
0485     }
0486   }
0487   SETFIELD("track", tracks.join(FieldFormat::rowDelimiterString()));
0488 
0489   // something special for compilations and such
0490   SETFIELD("artist", artist);
0491 #else
0492   Q_UNUSED(drive_);
0493 #endif
0494 }
0495 #undef SETFIELD
0496 
0497 QWidget* FreeDBImporter::widget(QWidget* parent_) {
0498   if(m_widget) {
0499     return m_widget;
0500   }
0501   m_widget = new QWidget(parent_);
0502   QVBoxLayout* l = new QVBoxLayout(m_widget);
0503 
0504   QGroupBox* gbox = new QGroupBox(i18n("Audio CD Options"), m_widget);
0505   QVBoxLayout* vlay = new QVBoxLayout(gbox);
0506 
0507   // cdrom stuff
0508   QHBoxLayout* hlay = new QHBoxLayout();
0509   vlay->addLayout(hlay);
0510 
0511   m_radioCDROM = new QRadioButton(i18n("Read data from CD-ROM device"), gbox);
0512   m_driveCombo = new KComboBox(true, gbox);
0513   m_driveCombo->setDuplicatesEnabled(false);
0514   QString w = i18n("Select or input the CD-ROM device location.");
0515   m_radioCDROM->setWhatsThis(w);
0516   m_driveCombo->setWhatsThis(w);
0517 
0518   hlay->addWidget(m_radioCDROM);
0519   hlay->addWidget(m_driveCombo);
0520 
0521   /********************************************************************************/
0522 
0523   m_radioCache = new QRadioButton(i18n("Read all CDDB cache files only"), gbox);
0524   m_radioCache->setWhatsThis(i18n("Read data recursively from all the CDDB cache files "
0525                                   "contained in the default cache folders."));
0526   vlay->addWidget(m_radioCache);
0527 
0528   // cddb cache stuff
0529   m_buttonGroup = new QButtonGroup(gbox);
0530   m_buttonGroup->addButton(m_radioCDROM);
0531   m_buttonGroup->addButton(m_radioCache);
0532 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
0533   void (QButtonGroup::* buttonClickedInt)(int) = &QButtonGroup::buttonClicked;
0534   connect(m_buttonGroup, buttonClickedInt, this, &FreeDBImporter::slotClicked);
0535 #else
0536   connect(m_buttonGroup, &QButtonGroup::idClicked, this, &FreeDBImporter::slotClicked);
0537 #endif
0538 
0539   l->addWidget(gbox);
0540   l->addStretch(1);
0541 
0542   // now read config options
0543   KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("ImportOptions - FreeDB"));
0544   QStringList devices = config.readEntry("CD-ROM Devices", QStringList());
0545   if(devices.isEmpty()) {
0546 #if defined(__OpenBSD__)
0547     devices += QLatin1String("/dev/rcd0c");
0548 #endif
0549     devices += QStringLiteral("/dev/cdrom");
0550     devices += QStringLiteral("/dev/dvd");
0551   }
0552   m_driveCombo->addItems(devices);
0553   QString device = config.readEntry("Last Device");
0554   if(!device.isEmpty()) {
0555     m_driveCombo->setEditText(device);
0556   }
0557   if(config.readEntry("Cache Files Only", false)) {
0558     m_radioCache->setChecked(true);
0559   } else {
0560     m_radioCDROM->setChecked(true);
0561   }
0562   // set enabled widgets
0563   slotClicked(m_buttonGroup->checkedId());
0564 
0565   return m_widget;
0566 }
0567 
0568 void FreeDBImporter::slotClicked(int id_) {
0569   QAbstractButton* button = m_buttonGroup->button(id_);
0570   if(!button) {
0571     return;
0572   }
0573 
0574   m_driveCombo->setEnabled(button == m_radioCDROM);
0575 }
0576 
0577 void FreeDBImporter::slotCancel() {
0578   m_cancelled = true;
0579 }