/* * Cantata * * Copyright (c) 2011-2012 Craig Drummond * * ---- * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "rgdialog.h" #ifdef ENABLE_DEVICES_SUPPORT #include "device.h" #endif #include "devicesmodel.h" #include "settings.h" #include "tags.h" #include "tagreader.h" #include "utils.h" #include "localize.h" #include "messagebox.h" #include "jobcontroller.h" #include #include #include #include #include #include #include #include #ifdef ENABLE_KDE_SUPPORT #include #endif #ifdef ENABLE_KDE_SUPPORT static QString formatNumber(double number, int precision) { return KGlobal::locale()->formatNumber(number, precision); } #else static QString formatNumber(double number, int precision) { return QString::number(number, 'g', precision); } #endif enum Columns { COL_ARTIST, COL_ALBUM, COL_TITLE, COL_ALBUMGAIN, COL_TRACKGAIN, COL_ALBUMPEAK, COL_TRACKPEAK }; static int iCount=0; static inline QString createAlbumName(const Song &s) { return s.albumArtist()+QLatin1String(" - ")+s.album; } int RgDialog::instanceCount() { return iCount; } RgDialog::RgDialog(QWidget *parent) : Dialog(parent) , state(State_Idle) , tagReader(0) { iCount++; setButtons(User1|Ok|Cancel); setCaption(i18n("ReplayGain")); setAttribute(Qt::WA_DeleteOnClose); QWidget *mainWidet = new QWidget(this); QBoxLayout *layout=new QBoxLayout(QBoxLayout::TopToBottom, mainWidet); view = new QTreeWidget(this); statusLabel = new QLabel(this); statusLabel->setVisible(false); progress = new QProgressBar(this); progress->setVisible(false); view->setRootIsDecorated(false); view->setAllColumnsShowFocus(true); QTreeWidgetItem *hdr = view->headerItem(); hdr->setText(COL_ARTIST, i18n("Artist")); hdr->setText(COL_ALBUM, i18n("Album")); hdr->setText(COL_TITLE, i18n("Title")); hdr->setText(COL_ALBUMGAIN, i18n("Album Gain")); hdr->setText(COL_TRACKGAIN, i18n("Track Gain")); hdr->setText(COL_ALBUMPEAK, i18n("Album Peak")); hdr->setText(COL_TRACKPEAK, i18n("Track Peak")); QHeaderView *hv=view->header(); hv->setResizeMode(COL_ARTIST, QHeaderView::ResizeToContents); hv->setResizeMode(COL_ALBUM, QHeaderView::ResizeToContents); hv->setResizeMode(COL_TITLE, QHeaderView::Stretch); hv->setResizeMode(COL_ALBUMGAIN, QHeaderView::Fixed); hv->setResizeMode(COL_TRACKGAIN, QHeaderView::Fixed); hv->setResizeMode(COL_ALBUMPEAK, QHeaderView::Fixed); hv->setResizeMode(COL_TRACKPEAK, QHeaderView::Fixed); hv->setStretchLastSection(false); layout->addWidget(view); layout->addWidget(statusLabel); layout->addWidget(progress); setMainWidget(mainWidet); #ifdef ENABLE_KDE_SUPPORT setButtonGuiItem(Ok, KStandardGuiItem::save()); setButtonGuiItem(Cancel, KStandardGuiItem::close()); #else setButtonGuiItem(Ok, KGuiItem(i18n("Save"), "document-save")); setButtonGuiItem(Cancel, KGuiItem(i18n("Close"), "dialog-close")); #endif setButtonGuiItem(User1, KGuiItem(i18n("Scan"), "edit-find")); enableButton(Ok, false); enableButton(User1, false); resize(800, 400); qRegisterMetaType("Tags::ReplayGain"); } RgDialog::~RgDialog() { clearScanners(); iCount--; } void RgDialog::show(const QList &songs, const QString &udi) { if (songs.count()<1) { deleteLater(); return; } origSongs=songs; qSort(origSongs); #ifdef ENABLE_DEVICES_SUPPORT if (udi.isEmpty()) { base=MPDConnection::self()->getDetails().dir; } else { Device *dev=getDevice(udi, parentWidget()); if (!dev) { deleteLater(); return; } base=dev->path(); } #else Q_UNUSED(udi) base=MPDConnection::self()->getDetails().dir; #endif state=State_Idle; enableButton(User1, origSongs.count()); view->clear(); int i=0; foreach (const Song &s, origSongs) { albums[createAlbumName(s)].tracks.append(i++); new QTreeWidgetItem(view, QStringList() << s.albumArtist() << s.album << s.title); } Dialog::show(); startReadingTags(); } void RgDialog::slotButtonClicked(int button) { if (State_Saving==state) { return; } switch (button) { case Ok: if (MessageBox::Yes==MessageBox::questionYesNo(this, i18n("Update ReplayGain tags in files?"))) { saveTags(); stopScanning(); accept(); } break; case User1: startScanning(); break; case Cancel: switch (state) { case State_ScanningFiles: if (MessageBox::Yes==MessageBox::questionYesNo(this, i18n("Cancel scanning of files?"))) { stopScanning(); // Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!! Dialog::slotButtonClicked(button); state=State_Idle; } break; case State_ScanningTags: if (MessageBox::Yes==MessageBox::questionYesNo(this, i18n("Cancel reading of existing tags?"))) { stopReadingTags(); // Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!! Dialog::slotButtonClicked(button); state=State_Idle; } break; default: case State_Idle: stopScanning(); reject(); // Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!! Dialog::slotButtonClicked(button); break; } break; default: break; } } void RgDialog::startScanning() { bool all=origTags.isEmpty() || MessageBox::Yes==MessageBox::questionYesNo(this, i18n("Do you wish to scan all files, or only files without existing tags?"), QString(), KGuiItem(i18n("All Tracks")), KGuiItem(i18n("Untagged Tracks"))); if (!all && origTags.count()==origSongs.count()) { return; } #ifdef ENABLE_KDE_SUPPORT setButtonGuiItem(Cancel, KStandardGuiItem::cancel()); #else setButtonGuiItem(Cancel, KGuiItem(i18n("Cancel"), "dialog-cancel")); #endif state=State_ScanningFiles; enableButton(Ok, false); enableButton(User1, false); progress->setVisible(true); statusLabel->setText(i18n("Scanning files...")); statusLabel->setVisible(true); clearScanners(); totalToScan=0; for (int i=0; isetRange(0, 100*totalToScan); } void RgDialog::stopScanning() { state=State_Idle; enableButton(User1, true); progress->setVisible(false); statusLabel->setVisible(false); JobController::self()->cancel(); clearScanners(); #ifdef ENABLE_KDE_SUPPORT setButtonGuiItem(Cancel, KStandardGuiItem::cancel()); #else setButtonGuiItem(Cancel, KGuiItem(i18n("Close"), "dialog-close")); #endif } void RgDialog::createScanner(int index) { Scanner *s=new Scanner(index); s->setFile(base+origSongs.at(index).file); connect(s, SIGNAL(progress(int)), this, SLOT(scannerProgress(int))); connect(s, SIGNAL(done()), this, SLOT(scannerDone())); scanners.insert(index, s); JobController::self()->add(s); } void RgDialog::clearScanners() { QMap::ConstIterator it(scanners.constBegin()); QMap::ConstIterator end(scanners.constEnd()); for (; it!=end; ++it) { it.value()->stop(); } scanners.clear(); toScan.clear(); tracks.clear(); QMap::iterator a(albums.begin()); QMap::iterator ae(albums.end()); for (; a!=ae; ++a) { (*a).scannedTracks.clear(); } } void RgDialog::startReadingTags() { if (tagReader) { return; } #ifdef ENABLE_KDE_SUPPORT setButtonGuiItem(Cancel, KStandardGuiItem::cancel()); #else setButtonGuiItem(Cancel, KGuiItem(i18n("Cancel"), "dialog-cancel")); #endif state=State_ScanningTags; enableButton(Ok, false); enableButton(User1, false); progress->setRange(0, origSongs.count()); progress->setVisible(true); statusLabel->setText(i18n("Reading existing tags...")); statusLabel->setVisible(true); tagReader=new TagReader(); tagReader->setDetails(origSongs, base); connect(tagReader, SIGNAL(progress(int, Tags::ReplayGain)), this, SLOT(songTags(int, Tags::ReplayGain))); connect(tagReader, SIGNAL(done()), this, SLOT(tagReaderDone())); JobController::self()->add(tagReader); } void RgDialog::stopReadingTags() { if (!tagReader) { return; } state=State_Idle; enableButton(User1, true); progress->setVisible(false); statusLabel->setVisible(false); disconnect(tagReader, SIGNAL(progress(int, Tags::ReplayGain)), this, SLOT(songTags(int, Tags::ReplayGain))); disconnect(tagReader, SIGNAL(done()), this, SLOT(tagReaderDone())); tagReader->requestAbort(); tagReader=0; } void RgDialog::saveTags() { state=State_Saving; enableButton(Ok, false); enableButton(Close, false); enableButton(Cancel, false); enableButton(User1, false); QStringList failed; QMap::iterator a(albums.begin()); QMap::iterator ae(albums.end()); int toSave=0; for (; a!=ae; ++a) { foreach (int idx, (*a).tracks) { if (needToSave.contains(idx)) { ++toSave; } } } progress->setVisible(true); progress->setRange(0, toSave); int count=0; for (a=albums.begin(); a!=ae; ++a) { foreach (int idx, (*a).tracks) { if (needToSave.contains(idx)) { const Track &track=tracks[idx]; if (track.success && Tags::Update_Failed==Tags::updateReplaygain(base+origSongs.at(idx).file, Tags::ReplayGain(Scanner::reference(track.data.loudness), Scanner::reference((*a).data.loudness), track.data.peakValue(), (*a).data.peakValue()))) { failed.append(origSongs.at(idx).file); } progress->setValue(progress->value()+1); if (0==count++%10) { QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } } } } if (failed.count()) { #ifdef ENABLE_KDE_SUPPORT MessageBox::errorList(this, i18n("Failed to update the tags of the following tracks:"), failed); #else MessageBox::errorList(this, QObject::tr("Failed to update the tags of some tracks."), failed); #endif } } void RgDialog::updateView() { int finished=0; quint64 totalProgress=0; QMap::iterator it=tracks.begin(); QMap::iterator end=tracks.end(); for (; it!=end; ++it) { if ((*it).finished) { finished++;; } totalProgress+=(*it).finished ? 100 : (*it).progress; } if (finished==totalToScan) { progress->setVisible(false); statusLabel->setVisible(false); state=State_Idle; #ifdef ENABLE_KDE_SUPPORT setButtonGuiItem(Cancel, KStandardGuiItem::close()); #else setButtonGuiItem(Cancel, KGuiItem(i18n("Close"), "dialog-close")); #endif enableButton(Ok, needToSave.count()); } else { progress->setValue(totalProgress); } } #ifdef ENABLE_DEVICES_SUPPORT Device * RgDialog::getDevice(const QString &udi, QWidget *p) { Device *dev=DevicesModel::self()->device(udi); if (!dev) { MessageBox::error(p ? p : this, i18n("Device has been removed!")); reject(); return 0; } if (!dev->isConnected()) { MessageBox::error(p ? p : this, i18n("Device is not connected.")); reject(); return 0; } if (!dev->isIdle()) { MessageBox::error(p ? p : this, i18n("Device is busy?")); reject(); return 0; } return dev; } #endif void RgDialog::scannerProgress(int p) { Scanner *s=qobject_cast(sender()); if (!s) { return; } tracks[s->index()].progress=p; updateView(); } void RgDialog::scannerDone() { Scanner *s=qobject_cast(sender()); if (!s) { return; } Track &track=tracks[s->index()]; if (!track.finished) { track.finished=true; track.success=s->success(); QTreeWidgetItem *item=view->topLevelItem(s->index()); if (s->success()) { track.progress=100; track.data=s->results(); item->setText(COL_TRACKGAIN, i18n("%1 dB").arg(formatNumber(Scanner::reference(track.data.loudness), 2))); item->setText(COL_TRACKPEAK, formatNumber(track.data.peakValue(), 6)); QFont f(font()); f.setItalic(true); if (origTags.contains(s->index())) { Tags::ReplayGain t=origTags[s->index()]; bool diff=false; if (!Utils::equal(t.trackGain, Scanner::reference(track.data.loudness), 0.01)) { item->setFont(COL_TRACKGAIN, f); diff=true; } if (!Utils::equal(t.trackPeak, track.data.peakValue(), 0.000001)) { item->setFont(COL_TRACKPEAK, f); diff=true; } if (diff) { item->setFont(COL_ARTIST, f); item->setFont(COL_TITLE, f); needToSave.insert(s->index()); } else { needToSave.remove(s->index()); } } else { item->setFont(COL_ARTIST, f); item->setFont(COL_TITLE, f); item->setFont(COL_TRACKGAIN, f); item->setFont(COL_TRACKPEAK, f); needToSave.insert(s->index()); } } else { item->setText(COL_TRACKGAIN, i18n("Failed")); item->setText(COL_TRACKPEAK, i18n("Failed")); needToSave.remove(s->index()); } QMap::iterator a(albums.find(createAlbumName(origSongs.at(s->index())))); QMap::iterator ae(albums.end()); if (a!=ae) { (*a).scannedTracks.insert(s->index()); if ((*a).scannedTracks.count()==(*a).tracks.count()) { QList as; foreach (int idx, (*a).tracks) { Scanner *s=scanners[idx]; if (s && s->success()) { as.append(s); } } if (as.count()) { QFont f(font()); f.setItalic(true); (*a).data=Scanner::global(as); foreach (int idx, (*a).tracks) { QTreeWidgetItem *item=view->topLevelItem(idx); item->setText(COL_ALBUMGAIN, i18n("%1 dB").arg(formatNumber(Scanner::reference((*a).data.loudness), 2))); item->setText(COL_ALBUMPEAK, formatNumber((*a).data.peak, 6)); if (origTags.contains(idx)) { Tags::ReplayGain t=origTags[idx]; bool diff=false; if (!Utils::equal(t.albumGain, Scanner::reference((*a).data.loudness), 0.01)) { item->setFont(COL_ALBUMGAIN, f); needToSave.insert(idx); diff=true; } if (!Utils::equal(t.albumPeak, (*a).data.peak, 0.000001)) { item->setFont(COL_ALBUMPEAK, f); needToSave.insert(idx); diff=true; } if (diff) { item->setFont(COL_ARTIST, f); item->setFont(COL_ALBUM, f); } } else { item->setFont(COL_ARTIST, f); item->setFont(COL_ALBUM, f); item->setFont(COL_ALBUMGAIN, f); item->setFont(COL_ALBUMPEAK, f); } } } foreach (int idx, (*a).tracks) { Scanner *s=scanners[idx]; if (s) { scanners.remove(idx); JobController::self()->finishedWith(s); } } } } updateView(); } if (!toScan.isEmpty()) { int index=toScan.takeAt(0); createScanner(index); } } void RgDialog::songTags(int index, Tags::ReplayGain tags) { if (index>=0 && indexsetValue(progress->value()+1); if (!tags.isEmpty()) { origTags[index]=tags; QTreeWidgetItem *item=view->topLevelItem(index); if (!item) { return; } item->setText(COL_TRACKGAIN, i18n("%1 dB").arg(formatNumber(tags.trackGain, 2))); item->setText(COL_TRACKPEAK, formatNumber(tags.trackPeak, 6)); item->setText(COL_ALBUMGAIN, i18n("%1 dB").arg(formatNumber(tags.albumGain, 2))); item->setText(COL_ALBUMPEAK, formatNumber(tags.albumPeak, 6)); } } } void RgDialog::tagReaderDone() { TagReader *t=qobject_cast(sender()); if (!t) { return; } t->stop(); tagReader=0; state=State_Idle; enableButton(User1, true); progress->setVisible(false); statusLabel->setVisible(false); } void RgDialog::closeEvent(QCloseEvent *event) { if (State_Idle!=state) { slotButtonClicked(Cancel); if (State_Idle!=state) { event->ignore(); } } else { Dialog::closeEvent(event); } }