File indexing completed on 2024-05-12 16:46:30

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 "../field.h"
0038 #include "../fieldformat.h"
0039 #include "../utils/tellico_utils.h"
0040 #include "../utils/string_utils.h"
0041 #include "../utils/guiproxy.h"
0042 #include "../progressmanager.h"
0043 #include "../utils/cursorsaver.h"
0044 #include "../tellico_debug.h"
0045 
0046 #if defined HAVE_KF5KCDDB
0047 #include <KCddb/Client>
0048 #elif defined HAVE_KCDDB
0049 #include <libkcddb/client.h>
0050 #endif
0051 
0052 #include <KComboBox>
0053 #include <KSharedConfig>
0054 #include <KConfigGroup>
0055 #include <KLocalizedString>
0056 
0057 #include <QInputDialog>
0058 #include <QFile>
0059 #include <QDir>
0060 #include <QLabel>
0061 #include <QGroupBox>
0062 #include <QRadioButton>
0063 #include <QButtonGroup>
0064 #include <QCheckBox>
0065 #include <QTextStream>
0066 #include <QVBoxLayout>
0067 #include <QTextCodec>
0068 #include <QApplication>
0069 
0070 using Tellico::Import::FreeDBImporter;
0071 
0072 FreeDBImporter::FreeDBImporter() : Tellico::Import::Importer()
0073     , m_widget(nullptr)
0074     , m_buttonGroup(nullptr)
0075     , m_radioCDROM(nullptr)
0076     , m_radioCache(nullptr)
0077     , m_driveCombo(nullptr)
0078     , m_cancelled(false) {
0079 }
0080 
0081 bool FreeDBImporter::canImport(int type) const {
0082   return type == Data::Collection::Album;
0083 }
0084 
0085 Tellico::Data::CollPtr FreeDBImporter::collection() {
0086   if(m_coll) {
0087     return m_coll;
0088   }
0089 
0090   m_cancelled = false;
0091   if(m_radioCDROM->isChecked()) {
0092     readCDROM();
0093   } else {
0094     readCache();
0095   }
0096   if(m_cancelled) {
0097     m_coll = Data::CollPtr();
0098   }
0099   return m_coll;
0100 }
0101 
0102 void FreeDBImporter::readCDROM() {
0103 #if defined (HAVE_KCDDB) || defined (HAVE_KF5KCDDB)
0104   QString drivePath = m_driveCombo->currentText();
0105   if(drivePath.isEmpty()) {
0106     setStatusMessage(i18n("<qt>Tellico was unable to access the CD-ROM device - <i>%1</i>.</qt>", drivePath));
0107     myDebug() << "no drive!";
0108     return;
0109   }
0110 
0111   // now it's ok to add device to saved list
0112   m_driveCombo->addItem(drivePath);
0113   QStringList drives;
0114   for(int i = 0; i < m_driveCombo->count(); ++i) {
0115     if(drives.indexOf(m_driveCombo->itemText(i)) == -1) {
0116       drives += m_driveCombo->itemText(i);
0117     }
0118   }
0119 
0120   {
0121     KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("ImportOptions - FreeDB"));
0122     config.writeEntry("CD-ROM Devices", drives);
0123     config.writeEntry("Last Device", drivePath);
0124     config.writeEntry("Cache Files Only", false);
0125   }
0126 
0127   QByteArray drive = QFile::encodeName(drivePath);
0128   QList<uint> lengths;
0129   KCDDB::TrackOffsetList list;
0130 #if 0
0131   // a1107d0a - Kruder & Dorfmeister - The K&D Sessions - Disc One.
0132 /*  list
0133     << 150      // First track start.
0134     << 29462
0135     << 66983
0136     << 96785
0137     << 135628
0138     << 168676
0139     << 194147
0140     << 222158
0141     << 247076
0142     << 278203   // Last track start.
0143     << 316732;  // Disc end.
0144 */
0145 /*
0146   list
0147     << 150      // First track start.
0148     << 3296
0149     << 14437
0150     << 41279
0151     << 51362
0152     << 56253
0153     << 59755
0154     << 61324
0155     << 66059
0156     << 69073
0157     << 77790
0158     << 83214
0159     << 89726
0160     << 92078
0161     << 106325
0162     << 113117
0163     << 116040
0164     << 119877
0165     << 124377
0166     << 145466
0167     << 157583
0168     << 167208
0169     << 173486
0170     << 180120
0171     << 185279
0172     << 193270
0173     << 206451
0174     << 217303   // Last track start.
0175     << 224925;  // Disc end.
0176   list
0177     << 150
0178     << 106965
0179     << 127220
0180     << 151925
0181     << 176085
0182     << 234500;
0183 */
0184 #else
0185   list = offsetList(drive, lengths);
0186 #endif
0187 
0188   if(list.isEmpty()) {
0189     setStatusMessage(i18n("<qt>Tellico was unable to access the CD-ROM device - <i>%1</i>.</qt>", drivePath));
0190     return;
0191   }
0192 //  myDebug() << list;
0193 
0194   // the result info, could be multiple ones
0195   KCDDB::CDInfo info;
0196   KCDDB::Client client;
0197   client.setBlockingMode(true);
0198 
0199   KCDDB::Result r = client.lookup(list);
0200   const KCDDB::CDInfoList responseList = client.lookupResponse();
0201   // KCDDB sometimes doesn't return MultipleRecordFound properly, so check length of response, too
0202   if(r == KCDDB::MultipleRecordFound || responseList.count() > 1) {
0203     QStringList list;
0204     foreach(const KCDDB::CDInfo& info, responseList) {
0205       list.append(QStringLiteral("%1, %2, %3").arg(info.get(KCDDB::Artist).toString(),
0206                                                         info.get(KCDDB::Title).toString(),
0207                                                         info.get(KCDDB::Genre).toString()));
0208     }
0209 
0210     // switch back to pointer cursor
0211     GUI::CursorSaver cs(Qt::ArrowCursor);
0212     bool ok;
0213     QString res = QInputDialog::getItem(GUI::Proxy::widget(),
0214                                         i18n("Select CDDB Entry"),
0215                                         i18n("Select a CDDB entry:"),
0216                                         list, 0, false, &ok);
0217     if(ok) {
0218       int i = 0;
0219       foreach(const QString& listValue, list) {
0220         if(listValue == res) {
0221           break;
0222         }
0223         ++i;
0224       }
0225       if(i < responseList.size()) {
0226         info = responseList.at(i);
0227       }
0228     } else { // cancelled dialog
0229       m_cancelled = true;
0230     }
0231   } else if(r == KCDDB::Success && !responseList.isEmpty()) {
0232     info = responseList.first();
0233   } else {
0234     myDebug() << "no success! Return value = " << r;
0235     QString s;
0236     switch(r) {
0237       case KCDDB::NoRecordFound:
0238         s = i18n("<qt>No records were found to match the CD.</qt>");
0239         break;
0240       case KCDDB::ServerError:
0241         myDebug() << "Server Error";
0242         break;
0243       case KCDDB::HostNotFound:
0244         myDebug() << "Host Not Found";
0245         break;
0246       case KCDDB::NoResponse:
0247         myDebug() << "No Response";
0248         break;
0249       case KCDDB::UnknownError:
0250         myDebug() << "Unknown Error";
0251         break;
0252       default:
0253         break;
0254     }
0255     if(s.isEmpty()) {
0256       s = i18n("<qt>Tellico was unable to complete the CD lookup.</qt>");
0257     }
0258     setStatusMessage(s);
0259     return;
0260   }
0261 
0262   if(!info.isValid()) {
0263     // go ahead and try to read cd-text if we weren't cancelled
0264     // could be the case we don't have net access
0265     if(!m_cancelled) {
0266       readCDText(drive);
0267     }
0268     return;
0269   }
0270 
0271   m_coll = new Data::MusicCollection(true);
0272 
0273   Data::EntryPtr entry(new Data::Entry(m_coll));
0274   // obviously a CD
0275   entry->setField(QStringLiteral("medium"), i18n("Compact Disc"));
0276   entry->setField(QStringLiteral("title"),  info.get(KCDDB::Title).toString());
0277   entry->setField(QStringLiteral("artist"), info.get(KCDDB::Artist).toString());
0278   entry->setField(QStringLiteral("genre"),  info.get(KCDDB::Genre).toString());
0279   if(!info.get(KCDDB::Year).isNull()) {
0280     entry->setField(QStringLiteral("year"), info.get(KCDDB::Year).toString());
0281   }
0282   entry->setField(QStringLiteral("keyword"), info.get(KCDDB::Category).toString());
0283   QString extd = info.get(QStringLiteral("EXTD")).toString();
0284   extd.replace(QLatin1Char('\n'), QLatin1String("<br/>"));
0285   entry->setField(QStringLiteral("comments"), extd);
0286 
0287   QStringList trackList;
0288   trackList.reserve(info.numberOfTracks());
0289   for(int i = 0; i < info.numberOfTracks(); ++i) {
0290     QString s = info.track(i).get(KCDDB::Title).toString() + FieldFormat::columnDelimiterString();
0291     const QString trackArtist = info.track(i).get(KCDDB::Artist).toString().trimmed();
0292     s += trackArtist.isEmpty() ? info.get(KCDDB::Artist).toString() : trackArtist;
0293     if(i < lengths.count()) {
0294       s += FieldFormat::columnDelimiterString() + Tellico::minutes(lengths[i]);
0295     }
0296     trackList << s;
0297   }
0298   entry->setField(QStringLiteral("track"), trackList.join(FieldFormat::rowDelimiterString()));
0299 
0300   m_coll->addEntries(entry);
0301   readCDText(drive);
0302 #endif
0303 }
0304 
0305 void FreeDBImporter::readCache() {
0306 #if defined (HAVE_KCDDB) || defined (HAVE_KF5KCDDB)
0307   {
0308     // remember the import options
0309     KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("ImportOptions - FreeDB"));
0310     config.writeEntry("Cache Files Only", true);
0311   }
0312 
0313   KCDDB::Config cfg;
0314   cfg.load();
0315 
0316   QStringList dirs = cfg.cacheLocations();
0317   foreach(const QString& dirName, dirs) {
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 }