add: reencode album feature

for now presets which are featured in the GUI are: mp3, aac, opus, lossless.

to-refactor: AlbumReencoder.cpp, backend code is currently longer than needed! we can shorten it. with seperating the raii handlers into a seperate class
This commit is contained in:
2026-06-24 12:46:20 +02:00
parent 66843e3ad2
commit d605509588
8 changed files with 996 additions and 13 deletions
+53
View File
@@ -2,6 +2,7 @@
#include "PlaybackController.h"
#include "LibraryController.h"
#include "NowPlayingPanel.h"
#include "ReencodeDialog.h"
#include "SpectrogramWidget.h"
#include "EqualizerWidget.h"
#include "PlaylistModel.h"
@@ -131,6 +132,14 @@ void MainWindow::buildMenuBar()
connect(refreshAct, &QAction::triggered, m_library, &LibraryController::scan);
file->addSeparator();
m_reencodeAct = file->addAction("Reencode Album…");
m_reencodeAct->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_R));
m_reencodeAct->setEnabled(false);
connect(m_reencodeAct, &QAction::triggered, this, &MainWindow::openReencodeDialog);
file->addSeparator();
auto *quitAct = file->addAction("Quit");
quitAct->setShortcut(QKeySequence::Quit);
connect(quitAct, &QAction::triggered, qApp, &QApplication::quit);
@@ -192,6 +201,8 @@ void MainWindow::wireSignals()
m_spectrogram->reset();
m_playlistView->scrollTo(
m_library->model()->index(m_playback->currentIndex()));
m_reencodeAct->setEnabled(t != nullptr);
});
// LibraryController -> ui status / track count
@@ -200,12 +211,54 @@ void MainWindow::wireSignals()
connect(m_library, &LibraryController::trackCountChanged,
m_playback, &PlaybackController::setTrackCount);
connect(m_library, &LibraryController::trackCountChanged,
this, [this](int count) {
m_reencodeAct->setEnabled(count > 0);
});
connect(m_playlistView, &QListView::doubleClicked,
this, [this](const QModelIndex &idx) {
m_playback->playTrack(idx.row());
});
}
void MainWindow::openReencodeDialog()
{
const QList<engine::Track> &all = m_library->library()->tracks();
if (all.isEmpty()) {
QMessageBox::information(this, "Reencode Album",
"No tracks in the library. Load a music folder first.");
return;
}
QString albumFilter;
int currentIdx = m_playback->currentIndex();
if (currentIdx >= 0 && currentIdx < all.size()) {
albumFilter = all[currentIdx].album;
} else {
QModelIndex sel = m_playlistView->currentIndex();
if (sel.isValid() && sel.row() < all.size())
albumFilter = all[sel.row()].album;
}
QList<engine::Track> targets;
if (!albumFilter.isEmpty() && albumFilter != "Unknown Album") {
for (const auto &t : all)
if (t.album.compare(albumFilter, Qt::CaseInsensitive) == 0)
targets.append(t);
}
if (targets.isEmpty())
targets = all;
ReencodeDialog dlg(targets, this);
if (!albumFilter.isEmpty() && albumFilter != "Unknown Album")
dlg.setWindowTitle(QString("Reencode Album — %1").arg(albumFilter));
dlg.exec();
}
void MainWindow::keyPressEvent(QKeyEvent *e)
{
+15 -13
View File
@@ -9,34 +9,36 @@ class SpectrogramWidget;
class EqualizerWidget;
class QListView;
class QLabel;
class QTabWidget;
class QAction;
class MainWindow : public QMainWindow
{
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
~MainWindow();
protected:
void closeEvent(QCloseEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
private slots:
void openReencodeDialog();
private:
void buildUI();
void buildMenuBar();
void wireSignals();
// Controllers
PlaybackController *m_playback;
LibraryController *m_library;
PlaybackController *m_playback { nullptr };
LibraryController *m_library { nullptr };
// Widgets
NowPlayingPanel *m_nowPlaying;
SpectrogramWidget *m_spectrogram;
EqualizerWidget *m_eqWidget;
QListView *m_playlistView;
QLabel *m_lblStatus;
NowPlayingPanel *m_nowPlaying { nullptr };
SpectrogramWidget *m_spectrogram { nullptr };
EqualizerWidget *m_eqWidget { nullptr };
QListView *m_playlistView{ nullptr };
QLabel *m_lblStatus { nullptr };
QAction *m_reencodeAct { nullptr };
QTimer m_uiTimer;
};
+261
View File
@@ -0,0 +1,261 @@
#include "ReencodeDialog.h"
#include "engine/AlbumReencoder.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QGroupBox>
#include <QComboBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTableWidget>
#include <QHeaderView>
#include <QProgressBar>
#include <QFileDialog>
#include <QMessageBox>
#include <QApplication>
#include <QDir>
#include <QFileInfo>
enum Col { ColArtist=0, ColTitle, ColStatus, ColOutput, COL_COUNT };
ReencodeDialog::ReencodeDialog(const QList<engine::Track> &tracks, QWidget *parent)
: QDialog(parent)
, m_tracks(tracks)
, m_presets(engine::builtinPresets())
, m_reencoder(new engine::AlbumReencoder(this))
{
setWindowTitle("Reencode Album");
setMinimumSize(820, 560);
buildUi();
populateTrackTable();
onPresetChanged(0);
connect(m_reencoder, &engine::AlbumReencoder::trackStarted,
this, &ReencodeDialog::onTrackStarted, Qt::QueuedConnection);
connect(m_reencoder, &engine::AlbumReencoder::trackFinished,
this, &ReencodeDialog::onTrackFinished, Qt::QueuedConnection);
connect(m_reencoder, &engine::AlbumReencoder::allFinished,
this, &ReencodeDialog::onAllFinished, Qt::QueuedConnection);
}
ReencodeDialog::~ReencodeDialog()
{
m_reencoder->requestStop();
}
void ReencodeDialog::buildUi()
{
auto *root = new QVBoxLayout(this);
root->setSpacing(10);
root->setContentsMargins(14, 14, 14, 14);
auto *settingsGroup = new QGroupBox("Encode Settings", this);
auto *settingsGrid = new QGridLayout(settingsGroup);
settingsGrid->setColumnStretch(1, 1);
settingsGrid->addWidget(new QLabel("Preset:", this), 0, 0);
m_presetCombo = new QComboBox(this);
for (const auto &p : m_presets)
m_presetCombo->addItem(p.label);
settingsGrid->addWidget(m_presetCombo, 0, 1, 1, 2);
m_presetDesc = new QLabel(this);
m_presetDesc->setWordWrap(true);
m_presetDesc->setStyleSheet("color: palette(mid);");
settingsGrid->addWidget(m_presetDesc, 1, 1, 1, 2);
settingsGrid->addWidget(new QLabel("Output folder:", this), 2, 0);
m_outputDir = new QLineEdit(this);
m_outputDir->setPlaceholderText("(same folder as source file)");
settingsGrid->addWidget(m_outputDir, 2, 1);
m_browseBtn = new QPushButton("Browse...", this);
m_browseBtn->setFixedWidth(90);
settingsGrid->addWidget(m_browseBtn, 2, 2);
root->addWidget(settingsGroup);
m_table = new QTableWidget(0, COL_COUNT, this);
m_table->setHorizontalHeaderLabels({"Artist", "Title", "Status", "Output"});
m_table->horizontalHeader()->setSectionResizeMode(ColArtist, QHeaderView::ResizeToContents);
m_table->horizontalHeader()->setSectionResizeMode(ColTitle, QHeaderView::Stretch);
m_table->horizontalHeader()->setSectionResizeMode(ColStatus, QHeaderView::ResizeToContents);
m_table->horizontalHeader()->setSectionResizeMode(ColOutput, QHeaderView::Stretch);
m_table->verticalHeader()->setVisible(false);
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
m_table->setAlternatingRowColors(true);
root->addWidget(m_table, 1);
auto *progressLayout = new QHBoxLayout;
m_statusLabel = new QLabel("Ready", this);
m_statusLabel->setMinimumWidth(120);
progressLayout->addWidget(m_statusLabel);
m_overallBar = new QProgressBar(this);
m_overallBar->setRange(0, 1);
m_overallBar->setValue(0);
m_overallBar->setTextVisible(true);
progressLayout->addWidget(m_overallBar, 1);
root->addLayout(progressLayout);
auto *btnLayout = new QHBoxLayout;
btnLayout->addStretch();
m_startStopBtn = new QPushButton("Start Encoding", this);
m_startStopBtn->setDefault(true);
m_startStopBtn->setMinimumWidth(130);
btnLayout->addWidget(m_startStopBtn);
m_closeBtn = new QPushButton("Close", this);
m_closeBtn->setMinimumWidth(80);
btnLayout->addWidget(m_closeBtn);
root->addLayout(btnLayout);
connect(m_presetCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ReencodeDialog::onPresetChanged);
connect(m_browseBtn, &QPushButton::clicked, this, &ReencodeDialog::onBrowseOutput);
connect(m_startStopBtn,&QPushButton::clicked, this, &ReencodeDialog::onStartStop);
connect(m_closeBtn, &QPushButton::clicked, this, &QDialog::reject);
}
void ReencodeDialog::populateTrackTable()
{
m_table->setRowCount(m_tracks.size());
for (int i = 0; i < m_tracks.size(); ++i) {
const auto &t = m_tracks[i];
m_table->setItem(i, ColArtist, new QTableWidgetItem(t.artist));
m_table->setItem(i, ColTitle, new QTableWidgetItem(t.displayTitle()));
m_table->setItem(i, ColStatus, new QTableWidgetItem("Pending"));
m_table->setItem(i, ColOutput, new QTableWidgetItem(""));
}
}
void ReencodeDialog::onPresetChanged(int index)
{
if (index < 0 || index >= m_presets.size()) return;
m_presetDesc->setText(m_presets[index].description);
}
void ReencodeDialog::onBrowseOutput()
{
QString startDir = m_outputDir->text();
if (startDir.isEmpty() && !m_tracks.isEmpty())
startDir = QFileInfo(m_tracks.first().filePath).absolutePath();
QString dir = QFileDialog::getExistingDirectory(
this, "Select Output Folder", startDir,
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (!dir.isEmpty())
m_outputDir->setText(dir);
}
void ReencodeDialog::onStartStop()
{
if (m_reencoder->isRunning()) {
m_reencoder->requestStop();
m_startStopBtn->setEnabled(false);
m_statusLabel->setText("Stopping...");
return;
}
int presetIdx = m_presetCombo->currentIndex();
if (presetIdx < 0) return;
const engine::EncodePreset &preset = m_presets[presetIdx];
QString outDir = m_outputDir->text().trimmed();
for (int i = 0; i < m_tracks.size(); ++i) {
m_table->item(i, ColStatus)->setText("Pending");
m_table->item(i, ColStatus)->setForeground(QApplication::palette().text());
m_table->item(i, ColOutput)->setText("");
}
QList<engine::ReencodeJob> jobs;
jobs.reserve(m_tracks.size());
for (const auto &t : m_tracks) {
engine::ReencodeJob job;
job.track = t;
job.preset = preset;
job.outputDir = outDir.isEmpty()
? QFileInfo(t.filePath).absolutePath()
: outDir;
jobs.append(job);
}
m_reencoder->setJobs(jobs);
m_doneCount = 0;
m_totalCount = jobs.size();
m_overallBar->setRange(0, m_totalCount);
m_overallBar->setValue(0);
setRunning(true);
m_reencoder->start();
}
void ReencodeDialog::onTrackStarted(int index, const QString &title)
{
m_statusLabel->setText(QString("Encoding %1/%2")
.arg(index + 1).arg(m_totalCount));
if (QTableWidgetItem *it = m_table->item(index, ColStatus)) {
it->setText("Encoding...");
}
m_table->scrollToItem(m_table->item(index, ColTitle));
Q_UNUSED(title)
}
void ReencodeDialog::onTrackFinished(int index, bool ok,
const QString &outPath,
const QString &err)
{
++m_doneCount;
updateOverallProgress();
if (QTableWidgetItem *st = m_table->item(index, ColStatus)) {
if (ok) {
st->setText("Done");
} else {
st->setText(QString("Failed: %1").arg(err));
}
}
if (ok) {
if (QTableWidgetItem *op = m_table->item(index, ColOutput))
op->setText(QFileInfo(outPath).fileName());
}
}
void ReencodeDialog::onAllFinished(int succeeded, int failed)
{
setRunning(false);
m_statusLabel->setText(
QString("Done - %1 succeeded, %2 failed").arg(succeeded).arg(failed));
m_overallBar->setValue(m_totalCount);
if (failed == 0) {
QMessageBox::information(this, "Reencode Complete",
QString("All %1 tracks encoded successfully.").arg(succeeded));
} else {
QMessageBox::warning(this, "Reencode Complete",
QString("%1 tracks succeeded, %2 failed.\n"
"Check the Status column for details.")
.arg(succeeded).arg(failed));
}
}
void ReencodeDialog::setRunning(bool running)
{
m_presetCombo->setEnabled(!running);
m_outputDir->setEnabled(!running);
m_browseBtn->setEnabled(!running);
m_startStopBtn->setText(running ? "Stop" : "Start Encoding");
m_startStopBtn->setEnabled(true);
m_closeBtn->setEnabled(!running);
}
void ReencodeDialog::updateOverallProgress()
{
m_overallBar->setValue(m_doneCount);
m_overallBar->setFormat(
QString("%1 / %2").arg(m_doneCount).arg(m_totalCount));
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <QDialog>
#include <QList>
#include "engine/Track.h"
#include "engine/ReencodeJob.h"
class QComboBox;
class QLineEdit;
class QPushButton;
class QTableWidget;
class QLabel;
class QProgressBar;
namespace engine { class AlbumReencoder; }
class ReencodeDialog : public QDialog {
Q_OBJECT
public:
explicit ReencodeDialog(const QList<engine::Track> &tracks,
QWidget *parent = nullptr);
~ReencodeDialog();
private slots:
void onPresetChanged(int index);
void onBrowseOutput();
void onStartStop();
void onTrackStarted (int index, const QString &title);
void onTrackFinished(int index, bool ok, const QString &outPath,
const QString &err);
void onAllFinished (int succeeded, int failed);
private:
void buildUi();
void populateTrackTable();
void setRunning(bool running);
void updateOverallProgress();
QList<engine::Track> m_tracks;
QList<engine::EncodePreset> m_presets;
QComboBox *m_presetCombo { nullptr };
QLabel *m_presetDesc { nullptr };
QLineEdit *m_outputDir { nullptr };
QPushButton *m_browseBtn { nullptr };
QTableWidget *m_table { nullptr };
QProgressBar *m_overallBar { nullptr };
QLabel *m_statusLabel { nullptr };
QPushButton *m_startStopBtn { nullptr };
QPushButton *m_closeBtn { nullptr };
engine::AlbumReencoder *m_reencoder { nullptr };
int m_doneCount { 0 };
int m_totalCount { 0 };
};