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:
@@ -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
@@ -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;
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user