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 }