Files
cantata/replaygain/rgdialog.cpp
Craig Drummond b6bd94c236 Update (c) year
2022-01-08 21:24:07 +00:00

672 lines
22 KiB
C++

/*
* Cantata
*
* Copyright (c) 2011-2022 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 "devices/device.h"
#include "models/devicesmodel.h"
#endif
#include "gui/settings.h"
#include "tags/tags.h"
#include "tagreader.h"
#include "support/utils.h"
#include "support/messagebox.h"
#include "support/monoicon.h"
#include "jobcontroller.h"
#include "widgets/basicitemdelegate.h"
#include "support/action.h"
#include "mpd-interface/cuefile.h"
#include <QComboBox>
#include <QTreeWidget>
#include <QLabel>
#include <QProgressBar>
#include <QBoxLayout>
#include <QHeaderView>
#include <QCloseEvent>
#include <QCoreApplication>
#include <QEventLoop>
#include <algorithm>
enum Columns
{
COL_ARTIST,
COL_ALBUM,
COL_TITLE,
COL_ALBUMGAIN,
COL_TRACKGAIN,
COL_ALBUMPEAK,
COL_TRACKPEAK
};
static int iCount=0;
int RgDialog::instanceCount()
{
return iCount;
}
static inline void setResizeMode(QHeaderView *hdr, int idx, QHeaderView::ResizeMode mode)
{
hdr->setSectionResizeMode(idx, mode);
}
RgDialog::RgDialog(QWidget *parent)
: SongDialog(parent, "RgDialog", QSize(800, 400))
, state(State_Idle)
, totalToScan(0)
, tagReader(0)
, autoScanTags(false)
{
iCount++;
setButtons(User1|Ok|Cancel);
setCaption(tr("ReplayGain"));
setAttribute(Qt::WA_DeleteOnClose);
QWidget *mainWidget = new QWidget(this);
QBoxLayout *layout=new QBoxLayout(QBoxLayout::TopToBottom, mainWidget);
combo = new QComboBox(this);
view = new QTreeWidget(this);
statusLabel = new QLabel(this);
statusLabel->setVisible(false);
progress = new QProgressBar(this);
progress->setVisible(false);
combo->addItem(tr("Show All Tracks"), true);
combo->addItem(tr("Show Untagged Tracks"), false);
view->setRootIsDecorated(false);
view->setAllColumnsShowFocus(true);
view->setItemDelegate(new BasicItemDelegate(view));
view->setAlternatingRowColors(false);
view->setContextMenuPolicy(Qt::ActionsContextMenu);
view->setSelectionMode(QAbstractItemView::ExtendedSelection);
removeAct=new Action(tr("Remove From List"), view);
removeAct->setEnabled(false);
view->addAction(removeAct);
QTreeWidgetItem *hdr = view->headerItem();
hdr->setText(COL_ARTIST, tr("Artist"));
hdr->setText(COL_ALBUM, tr("Album"));
hdr->setText(COL_TITLE, tr("Title"));
hdr->setText(COL_ALBUMGAIN, tr("Album Gain"));
hdr->setText(COL_TRACKGAIN, tr("Track Gain"));
hdr->setText(COL_ALBUMPEAK, tr("Album Peak"));
hdr->setText(COL_TRACKPEAK, tr("Track Peak"));
QHeaderView *hv=view->header();
setResizeMode(hv, COL_ARTIST, QHeaderView::ResizeToContents);
setResizeMode(hv, COL_ALBUM, QHeaderView::ResizeToContents);
setResizeMode(hv, COL_TITLE, QHeaderView::Stretch);
setResizeMode(hv, COL_ALBUMGAIN, QHeaderView::Fixed);
setResizeMode(hv, COL_TRACKGAIN, QHeaderView::Fixed);
setResizeMode(hv, COL_ALBUMPEAK, QHeaderView::Fixed);
setResizeMode(hv, COL_TRACKPEAK, QHeaderView::Fixed);
hv->setStretchLastSection(false);
layout->setMargin(0);
layout->addWidget(combo);
layout->addWidget(view);
layout->addWidget(statusLabel);
layout->addWidget(progress);
setMainWidget(mainWidget);
setButtonGuiItem(Ok, StdGuiItem::save());
setButtonGuiItem(Cancel, StdGuiItem::close());
setButtonGuiItem(User1, GuiItem(tr("Scan"), FontAwesome::search));
enableButton(Ok, false);
enableButton(User1, false);
qRegisterMetaType<Tags::ReplayGain>("Tags::ReplayGain");
connect(combo, SIGNAL(currentIndexChanged(int)), SLOT(toggleDisplay()));
connect(view, SIGNAL(itemSelectionChanged()), SLOT(controlRemoveAct()));
connect(removeAct, SIGNAL(triggered()), SLOT(removeItems()));
italic=font();
italic.setItalic(true);
JobController::self()->setMaxActive(1);
}
RgDialog::~RgDialog()
{
clearScanners();
iCount--;
}
void RgDialog::show(const QList<Song> &songs, const QString &udi, bool autoScan)
{
if (songs.isEmpty()) {
deleteLater();
return;
}
for (const Song &s: songs) {
if (!CueFile::isCue(s.file)) {
origSongs.append(s);
}
}
if (origSongs.isEmpty()) {
deleteLater();
return;
}
autoScanTags=autoScan;
std::sort(origSongs.begin(), origSongs.end());
#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
base=MPDConnection::self()->getDetails().dir;
#endif
if (!songsOk(origSongs, base, udi.isEmpty())) {
return;
}
state=State_Idle;
enableButton(User1, origSongs.count());
view->clear();
for (const Song &s: origSongs) {
if (s.isLocalFile()) {
base = QString();
}
QString aa = s.albumArtist();
const QString &al = s.album;
const QString &t = s.title;
if (aa.isEmpty() && al.isEmpty() && t.isEmpty()) {
new QTreeWidgetItem(view, QStringList() << "-" << "-" << Utils::getFile(s.file));
} else {
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, tr("Update ReplayGain tags in tracks?"), tr("Update Tags"),
// GuiItem(tr("Update Tags")), StdGuiItem::cancel())) {
if (saveTags()) {
stopScanning();
accept();
}
//}
break;
case User1:
startScanning();
break;
case Cancel:
switch (state) {
case State_ScanningFiles:
if (MessageBox::Yes==MessageBox::questionYesNo(this, tr("Abort scanning of tracks?"), tr("Abort"),
GuiItem(tr("Abort")), StdGuiItem::no())) {
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, tr("Abort reading of existing tags?"), tr("Abort"),
GuiItem(tr("Abort")), StdGuiItem::no())) {
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() ||
(origTags.count()==origSongs.count()
? MessageBox::Yes==MessageBox::questionYesNo(this, tr("Scan <b>all</b> tracks?<br/><br/><i>All tracks have existing ReplayGain tags.</i>"), QString(),
GuiItem(tr("Scan")), StdGuiItem::cancel())
: MessageBox::No==MessageBox::questionYesNo(this, tr("Do you wish to scan all tracks, or only tracks without existing tags?"), QString(),
GuiItem(tr("Untagged Tracks")), GuiItem(tr("All Tracks"))));
if (!all && origTags.count()==origSongs.count()) {
return;
}
setButtonGuiItem(Cancel, StdGuiItem::cancel());
state=State_ScanningFiles;
enableButton(Ok, false);
enableButton(User1, false);
progress->setValue(0);
progress->setVisible(true);
statusLabel->setText(tr("Scanning tracks..."));
statusLabel->setVisible(true);
clearScanners();
totalToScan=0;
QMap<QString, QList<int> > groupedTracks;
for (int i=0; i<origSongs.count(); ++i) {
if (!removedItems.contains(i) && (all || !origTags.contains(i))) {
const Song &sng=origSongs.at(i);
QString aa = sng.albumArtist();
const QString &al = sng.album;
groupedTracks[aa.isEmpty() && al.isEmpty() ? (Utils::getFile(sng.file)+" -- "+QString::number(i)) : (sng.albumArtist()+" -- "+sng.album)].append(i);
}
}
QMap<QString, QList<int> >::ConstIterator it(groupedTracks.constBegin());
QMap<QString, QList<int> >::ConstIterator end(groupedTracks.constEnd());
for (; it!=end; ++it) {
createScanner(*it);
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();
setButtonGuiItem(Cancel, StdGuiItem::close());
}
void RgDialog::createScanner(const QList<int> &indexes)
{
QMap<int, QString> fileMap;
for (int i: indexes) {
fileMap[i]=origSongs.at(i).filePath(base);
}
AlbumScanner *s=new AlbumScanner(fileMap);
connect(s, SIGNAL(progress(int)), this, SLOT(scannerProgress(int)));
connect(s, SIGNAL(done()), this, SLOT(scannerDone()));
scanners[s]=0;
JobController::self()->add(s);
}
void RgDialog::clearScanners()
{
QList<AlbumScanner *> scannerList=scanners.keys();
for (AlbumScanner *sc: scannerList) {
sc->stop();
}
scanners.clear();
}
void RgDialog::startReadingTags()
{
if (tagReader) {
return;
}
setButtonGuiItem(Cancel, StdGuiItem::cancel());
state=State_ScanningTags;
enableButton(Ok, false);
enableButton(User1, false);
progress->setRange(0, origSongs.count());
progress->setVisible(true);
statusLabel->setText(tr("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;
autoScanTags=false;
}
bool RgDialog::saveTags()
{
state=State_Saving;
enableButton(Ok, false);
enableButton(Close, false);
enableButton(Cancel, false);
enableButton(User1, false);
QStringList failed;
progress->setVisible(true);
progress->setRange(0, tagsToSave.count());
int count=0;
bool someTimedout=false;
QMap<int, Tags::ReplayGain>::ConstIterator it=tagsToSave.constBegin();
QMap<int, Tags::ReplayGain>::ConstIterator end=tagsToSave.constEnd();
for (; it!=end; ++it) {
QString filePath=origSongs.at(it.key()).filePath(base);
switch (Tags::updateReplaygain(filePath, it.value())) {
case Tags::Update_Failed:
failed.append(filePath);
break;
case Tags::Update_BadFile:
failed.append(tr("%1 (Corrupt tags?)", "filename (Corrupt tags?)").arg(filePath));
break;
default:
break;
}
progress->setValue(progress->value()+1);
if (0==count++%10) {
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
}
if (failed.count()) {
MessageBox::errorListEx(this, tr("Failed to update the tags of the following tracks:"), failed);
}
return !someTimedout;
}
void RgDialog::updateView()
{
int finished=0;
quint64 totalProgress=0;
QMap<AlbumScanner *, int>::ConstIterator it=scanners.constBegin();
QMap<AlbumScanner *, int>::ConstIterator end=scanners.constEnd();
for (; it!=end; ++it) {
if (100==it.value()) {
finished++;
}
totalProgress+=(*it);
}
if (finished==totalToScan) {
progress->setVisible(false);
statusLabel->setVisible(false);
state=State_Idle;
setButtonGuiItem(Cancel, StdGuiItem::close());
enableButton(Ok, !tagsToSave.isEmpty());
} 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, tr("Device has been removed!"));
reject();
return 0;
}
if (!dev->isConnected()) {
MessageBox::error(p ? p : this, tr("Device is not connected."));
reject();
return 0;
}
if (!dev->isIdle()) {
MessageBox::error(p ? p : this, tr("Device is busy?"));
reject();
return 0;
}
return dev;
}
#endif
void RgDialog::scannerProgress(int p)
{
AlbumScanner *s=qobject_cast<AlbumScanner *>(sender());
if (!s) {
return;
}
scanners[s]=p;
updateView();
}
void RgDialog::scannerDone()
{
AlbumScanner *s=qobject_cast<AlbumScanner *>(sender());
if (!s) {
return;
}
const QMap<int, AlbumScanner::Values> &trackValues=s->trackValues();
QMap<int, AlbumScanner::Values>::ConstIterator it(trackValues.constBegin());
QMap<int, AlbumScanner::Values>::ConstIterator end(trackValues.constEnd());
if (s->success()) {
for(; it!=end; ++it) {
Tags::ReplayGain updatedTags(it.value().gain, s->albumValues().gain, it.value().peak, s->albumValues().peak);
QTreeWidgetItem *item=view->topLevelItem(it.key());
if (it.value().ok) {
item->setText(COL_TRACKGAIN, tr("%1 dB").arg(Utils::formatNumber(updatedTags.trackGain, 2)));
if (!Utils::equal(updatedTags.trackPeak, 0.0)) {
item->setText(COL_TRACKPEAK, Utils::formatNumber(updatedTags.trackPeak, 6));
}
} else {
item->setText(COL_TRACKGAIN, tr("Failed"));
item->setText(COL_TRACKPEAK, tr("Failed"));
}
if (s->albumValues().ok) {
item->setText(COL_ALBUMGAIN, tr("%1 dB").arg(Utils::formatNumber(updatedTags.albumGain, 2)));
if (!Utils::equal(updatedTags.albumPeak, 0.0)) {
item->setText(COL_ALBUMPEAK, Utils::formatNumber(updatedTags.albumPeak, 6));
}
} else {
item->setText(COL_ALBUMGAIN, tr("Failed"));
item->setText(COL_ALBUMPEAK, tr("Failed"));
}
if (it.value().ok && origTags.contains(it.key())) {
Tags::ReplayGain t=origTags[it.key()];
bool diff=false;
bool diffAlbum=false;
if (!Utils::equal(t.trackGain, updatedTags.trackGain, 0.01)) {
item->setFont(COL_TRACKGAIN, italic);
item->setToolTip(COL_TRACKGAIN, tr("Original: %1 dB").arg(Utils::formatNumber(t.trackGain, 2)));
diff=true;
}
if (!Utils::equal(updatedTags.trackPeak, 0.0) && !Utils::equal(t.trackPeak, updatedTags.trackPeak, 0.000001)) {
item->setFont(COL_TRACKPEAK, italic);
item->setToolTip(COL_TRACKPEAK, tr("Original: %1").arg(Utils::formatNumber(t.trackPeak, 6)));
diff=true;
}
if (!Utils::equal(t.albumGain, updatedTags.albumGain, 0.01)) {
item->setFont(COL_ALBUMGAIN, italic);
item->setToolTip(COL_ALBUMGAIN, tr("Original: %1 dB").arg(Utils::formatNumber(t.albumGain, 2)));
diffAlbum=true;
}
if (!Utils::equal(updatedTags.albumPeak, 0.0) && !Utils::equal(t.albumPeak, updatedTags.albumPeak, 0.000001)) {
item->setFont(COL_ALBUMPEAK, italic);
item->setToolTip(COL_ALBUMPEAK, tr("Original: %1").arg(Utils::formatNumber(t.albumPeak, 6)));
diffAlbum=true;
}
if (diff || diffAlbum) {
if (diff) {
item->setFont(COL_ARTIST, italic);
item->setFont(COL_TITLE, italic);
}
if (diffAlbum) {
item->setFont(COL_ALBUM, italic);
}
tagsToSave.insert(it.key(), updatedTags);
} else {
tagsToSave.remove(it.key());
}
} else if (it.value().ok) {
item->setFont(COL_ARTIST, italic);
item->setFont(COL_ALBUM, italic);
item->setFont(COL_TITLE, italic);
item->setFont(COL_TRACKGAIN, italic);
item->setFont(COL_TRACKPEAK, italic);
item->setFont(COL_ALBUMGAIN, italic);
item->setFont(COL_ALBUMPEAK, italic);
tagsToSave.insert(it.key(), updatedTags);
} else {
tagsToSave.remove(it.key());
}
}
} else {
for(; it!=end; ++it) {
QTreeWidgetItem *item=view->topLevelItem(it.key());
item->setText(COL_TRACKGAIN, tr("Failed"));
item->setText(COL_TRACKPEAK, tr("Failed"));
item->setText(COL_ALBUMGAIN, tr("Failed"));
item->setText(COL_ALBUMPEAK, tr("Failed"));
tagsToSave.remove(it.key());
}
}
scanners[s]=100;
updateView();
JobController::self()->finishedWith(s);
}
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, tr("%1 dB").arg(Utils::formatNumber(tags.trackGain, 2)));
if (!Utils::equal(tags.trackPeak, 0.0)) {
item->setText(COL_TRACKPEAK, Utils::formatNumber(tags.trackPeak, 6));
}
item->setText(COL_ALBUMGAIN, tr("%1 dB").arg(Utils::formatNumber(tags.albumGain, 2)));
if (!Utils::equal(tags.albumPeak, 0.0)) {
item->setText(COL_ALBUMPEAK, Utils::formatNumber(tags.albumPeak, 6));
}
}
}
}
void RgDialog::tagReaderDone()
{
TagReader *t=qobject_cast<TagReader *>(sender());
if (!t) {
return;
}
JobController::self()->finishedWith(t);
tagReader=0;
state=State_Idle;
enableButton(User1, true);
progress->setVisible(false);
statusLabel->setVisible(false);
if (autoScanTags) {
autoScanTags=false;
startScanning();
}
}
void RgDialog::toggleDisplay()
{
bool showAll=combo->itemData(combo->currentIndex()).toBool();
for (int i=0; i<view->topLevelItemCount(); ++i) {
QTreeWidgetItem *item=view->topLevelItem(i);
if (!removedItems.contains(view->indexOfTopLevelItem(item))) {
item->setHidden(showAll ? false : origTags.contains(i));
}
}
}
void RgDialog::controlRemoveAct()
{
removeAct->setEnabled(view->topLevelItemCount()>1 && !view->selectedItems().isEmpty());
}
void RgDialog::removeItems()
{
if (view->topLevelItemCount()<1) {
return;
}
if (MessageBox::Yes==MessageBox::questionYesNo(this, tr("Remove the selected tracks from the list?"),
tr("Remove Tracks"), StdGuiItem::remove(), StdGuiItem::cancel())) {
QList<QTreeWidgetItem *> selection=view->selectedItems();
for (QTreeWidgetItem *item: selection) {
int index=view->indexOfTopLevelItem(item);
item->setHidden(true);
removedItems.insert(index);
if (tagsToSave.contains(index)) {
tagsToSave.remove(index);
}
}
}
}
void RgDialog::closeEvent(QCloseEvent *event)
{
if (State_Idle!=state) {
slotButtonClicked(Cancel);
if (State_Idle!=state) {
event->ignore();
}
} else {
Dialog::closeEvent(event);
}
}
#include "moc_rgdialog.cpp"