Files
cantata/replaygain/rgdialog.cpp

630 lines
19 KiB
C++

/*
* Cantata
*
* Copyright (c) 2011-2012 Craig Drummond <craig.p.drummond@gmail.com>
*
* ----
*
* 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 <QtGui/QTreeWidget>
#include <QtGui/QLabel>
#include <QtGui/QProgressBar>
#include <QtGui/QBoxLayout>
#include <QtGui/QHeaderView>
#include <QtGui/QCloseEvent>
#include <QtCore/QCoreApplication>
#include <QtCore/QEventLoop>
#ifdef ENABLE_KDE_SUPPORT
#include <KDE/KLocale>
#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>("Tags::ReplayGain");
}
RgDialog::~RgDialog()
{
clearScanners();
iCount--;
}
void RgDialog::show(const QList<Song> &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; i<origSongs.count(); ++i) {
if (all || !origTags.contains(i)) {
if (scanners.count()<100) {
createScanner(i);
} else {
toScan.append(i);
}
totalToScan++;
}
}
progress->setRange(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<int, Scanner *>::ConstIterator it(scanners.constBegin());
QMap<int, Scanner *>::ConstIterator end(scanners.constEnd());
for (; it!=end; ++it) {
it.value()->stop();
}
scanners.clear();
toScan.clear();
tracks.clear();
QMap<QString, Album>::iterator a(albums.begin());
QMap<QString, Album>::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<QString, Album>::iterator a(albums.begin());
QMap<QString, Album>::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<int, Track>::iterator it=tracks.begin();
QMap<int, Track>::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<Scanner *>(sender());
if (!s) {
return;
}
tracks[s->index()].progress=p;
updateView();
}
void RgDialog::scannerDone()
{
Scanner *s=qobject_cast<Scanner *>(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<QString, Album>::iterator a(albums.find(createAlbumName(origSongs.at(s->index()))));
QMap<QString, Album>::iterator ae(albums.end());
if (a!=ae) {
(*a).scannedTracks.insert(s->index());
if ((*a).scannedTracks.count()==(*a).tracks.count()) {
QList<Scanner *> 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 && index<origSongs.count()) {
progress->setValue(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<TagReader *>(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);
}
}