Initial auCDtect Linux implementation
This commit is contained in:
670
src/MainWindow.cpp
Normal file
670
src/MainWindow.cpp
Normal file
@@ -0,0 +1,670 @@
|
||||
#include "MainWindow.h"
|
||||
|
||||
#include <QCloseEvent>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QDirIterator>
|
||||
#include <QFormLayout>
|
||||
#include <QFutureWatcher>
|
||||
#include <QGroupBox>
|
||||
#include <QHeaderView>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QProcess>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QSpinBox>
|
||||
#include <QSplitter>
|
||||
#include <QStandardPaths>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QTextStream>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
|
||||
namespace {
|
||||
constexpr auto kOrgName = "aucdtect-linux";
|
||||
constexpr auto kAppName = "aucdtect-linux";
|
||||
constexpr auto kDecoderTemplateKey = "decoderCommand";
|
||||
constexpr auto kReportPathKey = "reportPath";
|
||||
constexpr auto kParallelismKey = "parallelism";
|
||||
constexpr auto kStatusPending = "Pending";
|
||||
constexpr auto kStatusRunning = "Running";
|
||||
constexpr auto kStatusDone = "Done";
|
||||
constexpr auto kStatusFailed = "Failed";
|
||||
constexpr auto kStatusStopped = "Stopped";
|
||||
constexpr auto kDefaultDecoderTemplate = "ffmpeg -loglevel error -y -i {input} -map 0:a:0 -vn -sn -dn -ar 44100 -ac 2 -c:a pcm_s16le {decoded}";
|
||||
|
||||
const QStringList kAudioNameFilters = {
|
||||
"*.wav", "*.flac", "*.ape", "*.wv", "*.tak", "*.tta", "*.m4a", "*.alac", "*.shn", "*.ofr", "*.ofs", "*.mp3", "*.aac"
|
||||
};
|
||||
|
||||
bool isWavInput(const QString &path) {
|
||||
return QFileInfo(path).suffix().compare("wav", Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
QString computeFileHash(const QString &path) {
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QCryptographicHash hash(QCryptographicHash::Md5);
|
||||
while (!file.atEnd()) {
|
||||
hash.addData(file.read(1 << 20));
|
||||
}
|
||||
return hash.result().toHex();
|
||||
}
|
||||
|
||||
QString computeSignature(const QString &inputPath, const QString &conclusion, const QString &accuracy, const QString &fileHash) {
|
||||
const QString material = inputPath + "|" + conclusion + "|" + accuracy + "|" + fileHash;
|
||||
return QCryptographicHash::hash(material.toUtf8(), QCryptographicHash::Sha1).toHex();
|
||||
}
|
||||
|
||||
struct WorkerPayload {
|
||||
int taskIndex = -1;
|
||||
QString inputPath;
|
||||
QString decoderTemplate;
|
||||
QString reportPath;
|
||||
};
|
||||
|
||||
QString escapeShellArgumentStandalone(const QString &value) {
|
||||
QString escaped = value;
|
||||
escaped.replace('\'', "'\"'\"'");
|
||||
return "'" + escaped + "'";
|
||||
}
|
||||
|
||||
QString applyPlaceholders(QString command, const QString &inputPath, const QString &decodedPath, const QString &reportPath) {
|
||||
command.replace("{input}", escapeShellArgumentStandalone(inputPath));
|
||||
command.replace("{decoded}", escapeShellArgumentStandalone(decodedPath));
|
||||
command.replace("{report}", escapeShellArgumentStandalone(reportPath));
|
||||
return command;
|
||||
}
|
||||
|
||||
WorkerResult runWorker(const WorkerPayload &payload) {
|
||||
WorkerResult result;
|
||||
result.status = kStatusFailed;
|
||||
|
||||
const QString baseName = QFileInfo(payload.inputPath).baseName();
|
||||
const QString cacheBase = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
QDir().mkpath(cacheBase);
|
||||
const QString tempDirPath = cacheBase + "/" + baseName + "-" + QString::number(QDateTime::currentMSecsSinceEpoch()) +
|
||||
"-" + QString::number(payload.taskIndex);
|
||||
QDir().mkpath(tempDirPath);
|
||||
|
||||
const QString decodedPath = isWavInput(payload.inputPath) ? payload.inputPath : tempDirPath + "/decoded.wav";
|
||||
QString externalOutput;
|
||||
|
||||
if (!isWavInput(payload.inputPath)) {
|
||||
if (payload.decoderTemplate.trimmed().isEmpty()) {
|
||||
result.conclusion = "Decoder command is required for non-WAV input";
|
||||
result.analyzerOutput = result.conclusion;
|
||||
QDir(tempDirPath).removeRecursively();
|
||||
return result;
|
||||
}
|
||||
|
||||
QProcess decoder;
|
||||
decoder.setProcessChannelMode(QProcess::MergedChannels);
|
||||
const QString reportPath = payload.reportPath.isEmpty() ? QDir(tempDirPath).filePath("analysis-report.txt") : payload.reportPath;
|
||||
const QString command = applyPlaceholders(payload.decoderTemplate, payload.inputPath, decodedPath, reportPath);
|
||||
decoder.start("/bin/sh", {"-lc", command});
|
||||
if (!decoder.waitForStarted()) {
|
||||
result.conclusion = "Decoder failed to start";
|
||||
result.analyzerOutput = result.conclusion;
|
||||
QDir(tempDirPath).removeRecursively();
|
||||
return result;
|
||||
}
|
||||
decoder.waitForFinished(-1);
|
||||
result.exitCode = decoder.exitCode();
|
||||
externalOutput = QString::fromLocal8Bit(decoder.readAll());
|
||||
if (decoder.exitStatus() != QProcess::NormalExit || decoder.exitCode() != 0) {
|
||||
result.conclusion = "Decoder failed";
|
||||
result.analyzerOutput = externalOutput;
|
||||
QDir(tempDirPath).removeRecursively();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
AudioAnalyzer analyzer;
|
||||
const AudioAnalysisReport report = analyzer.analyzeFile(decodedPath);
|
||||
if (!report.ok) {
|
||||
result.conclusion = report.error;
|
||||
result.analyzerOutput = report.rawReport.isEmpty() ? report.error : report.rawReport;
|
||||
QDir(tempDirPath).removeRecursively();
|
||||
return result;
|
||||
}
|
||||
|
||||
result.status = kStatusDone;
|
||||
result.exitCode = 0;
|
||||
result.conclusion = report.conclusion;
|
||||
result.accuracy = report.accuracy;
|
||||
result.fileHash = computeFileHash(payload.inputPath);
|
||||
result.signature = computeSignature(payload.inputPath, report.conclusion, report.accuracy, result.fileHash);
|
||||
result.analyzerOutput = report.rawReport;
|
||||
if (!externalOutput.trimmed().isEmpty()) {
|
||||
result.analyzerOutput += "\n\nDecoder output\n--------------\n" + externalOutput.trimmed() + "\n";
|
||||
}
|
||||
|
||||
QDir(tempDirPath).removeRecursively();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow::MainWindow() {
|
||||
buildUi();
|
||||
loadSettings();
|
||||
connectSignals();
|
||||
updateSummary();
|
||||
updateDetailsPanel();
|
||||
appendLog("Ready. Internal analysis is built in, worker count controls concurrent jobs.");
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() = default;
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *event) {
|
||||
saveSettings();
|
||||
QMainWindow::closeEvent(event);
|
||||
}
|
||||
|
||||
void MainWindow::buildUi() {
|
||||
setWindowTitle("auCDtect Linux");
|
||||
resize(1360, 860);
|
||||
|
||||
auto *central = new QWidget(this);
|
||||
auto *mainLayout = new QVBoxLayout(central);
|
||||
|
||||
auto *toolbarLayout = new QHBoxLayout();
|
||||
addFilesButton_ = new QPushButton("Add Files", this);
|
||||
addFolderButton_ = new QPushButton("Add Folder", this);
|
||||
clearButton_ = new QPushButton("Clear", this);
|
||||
startButton_ = new QPushButton("Start", this);
|
||||
stopButton_ = new QPushButton("Stop", this);
|
||||
exportButton_ = new QPushButton("Save Report", this);
|
||||
toolbarLayout->addWidget(addFilesButton_);
|
||||
toolbarLayout->addWidget(addFolderButton_);
|
||||
toolbarLayout->addWidget(clearButton_);
|
||||
toolbarLayout->addStretch();
|
||||
toolbarLayout->addWidget(startButton_);
|
||||
toolbarLayout->addWidget(stopButton_);
|
||||
toolbarLayout->addWidget(exportButton_);
|
||||
|
||||
auto *engineGroup = new QGroupBox("Engine Setup", this);
|
||||
auto *engineLayout = new QFormLayout(engineGroup);
|
||||
decoderCommandEdit_ = new QLineEdit(this);
|
||||
reportPathEdit_ = new QLineEdit(this);
|
||||
parallelismSpin_ = new QSpinBox(this);
|
||||
parallelismSpin_->setRange(1, 32);
|
||||
parallelismSpin_->setValue(1);
|
||||
decoderCommandEdit_->setPlaceholderText(kDefaultDecoderTemplate);
|
||||
reportPathEdit_->setPlaceholderText("Optional shared report path");
|
||||
engineLayout->addRow("Decoder", decoderCommandEdit_);
|
||||
engineLayout->addRow("Workers", parallelismSpin_);
|
||||
engineLayout->addRow("Report File", reportPathEdit_);
|
||||
|
||||
auto *summaryGroup = new QGroupBox("Task Summary", this);
|
||||
auto *summaryLayout = new QHBoxLayout(summaryGroup);
|
||||
pendingLabel_ = new QLabel(this);
|
||||
runningLabel_ = new QLabel(this);
|
||||
doneLabel_ = new QLabel(this);
|
||||
failedLabel_ = new QLabel(this);
|
||||
summaryLayout->addWidget(pendingLabel_);
|
||||
summaryLayout->addWidget(runningLabel_);
|
||||
summaryLayout->addWidget(doneLabel_);
|
||||
summaryLayout->addWidget(failedLabel_);
|
||||
summaryLayout->addStretch();
|
||||
|
||||
taskTable_ = new QTableWidget(this);
|
||||
taskTable_->setColumnCount(6);
|
||||
taskTable_->setHorizontalHeaderLabels({"File", "Status", "Conclusion", "Accuracy", "Hash", "Exit"});
|
||||
taskTable_->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
taskTable_->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||
taskTable_->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||
taskTable_->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
|
||||
taskTable_->horizontalHeader()->setSectionResizeMode(4, QHeaderView::Stretch);
|
||||
taskTable_->horizontalHeader()->setSectionResizeMode(5, QHeaderView::ResizeToContents);
|
||||
taskTable_->verticalHeader()->setVisible(false);
|
||||
taskTable_->setAlternatingRowColors(true);
|
||||
taskTable_->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
taskTable_->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
|
||||
detailsView_ = new QPlainTextEdit(this);
|
||||
detailsView_->setReadOnly(true);
|
||||
detailsView_->setPlaceholderText("Select a task to inspect the detailed report.");
|
||||
|
||||
auto *detailsGroup = new QGroupBox("Selected Task", this);
|
||||
auto *detailsLayout = new QVBoxLayout(detailsGroup);
|
||||
detailsLayout->addWidget(detailsView_);
|
||||
|
||||
auto *topSplitter = new QSplitter(Qt::Horizontal, this);
|
||||
topSplitter->addWidget(taskTable_);
|
||||
topSplitter->addWidget(detailsGroup);
|
||||
topSplitter->setStretchFactor(0, 3);
|
||||
topSplitter->setStretchFactor(1, 2);
|
||||
|
||||
logView_ = new QPlainTextEdit(this);
|
||||
logView_->setReadOnly(true);
|
||||
logView_->setPlaceholderText("Execution log");
|
||||
|
||||
auto *bottomGroup = new QGroupBox("Run Log", this);
|
||||
auto *bottomLayout = new QVBoxLayout(bottomGroup);
|
||||
bottomLayout->addWidget(logView_);
|
||||
|
||||
auto *mainSplitter = new QSplitter(Qt::Vertical, this);
|
||||
mainSplitter->addWidget(topSplitter);
|
||||
mainSplitter->addWidget(bottomGroup);
|
||||
mainSplitter->setStretchFactor(0, 4);
|
||||
mainSplitter->setStretchFactor(1, 2);
|
||||
|
||||
auto *topMetaLayout = new QHBoxLayout();
|
||||
topMetaLayout->addWidget(engineGroup, 2);
|
||||
topMetaLayout->addWidget(summaryGroup, 1);
|
||||
|
||||
mainLayout->addLayout(toolbarLayout);
|
||||
mainLayout->addLayout(topMetaLayout);
|
||||
mainLayout->addWidget(mainSplitter);
|
||||
|
||||
setCentralWidget(central);
|
||||
stopButton_->setEnabled(false);
|
||||
}
|
||||
|
||||
void MainWindow::loadSettings() {
|
||||
QSettings settings(kOrgName, kAppName);
|
||||
decoderCommandEdit_->setText(settings.value(kDecoderTemplateKey, kDefaultDecoderTemplate).toString());
|
||||
reportPathEdit_->setText(settings.value(kReportPathKey).toString());
|
||||
parallelismSpin_->setValue(settings.value(kParallelismKey, 1).toInt());
|
||||
}
|
||||
|
||||
void MainWindow::saveSettings() const {
|
||||
QSettings settings(kOrgName, kAppName);
|
||||
settings.setValue(kDecoderTemplateKey, decoderCommandEdit_->text().trimmed());
|
||||
settings.setValue(kReportPathKey, reportPathEdit_->text().trimmed());
|
||||
settings.setValue(kParallelismKey, parallelismSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::connectSignals() {
|
||||
connect(addFilesButton_, &QPushButton::clicked, this, [this] { addFiles(); });
|
||||
connect(addFolderButton_, &QPushButton::clicked, this, [this] { addFolder(); });
|
||||
connect(clearButton_, &QPushButton::clicked, this, [this] { clearTasks(); });
|
||||
connect(startButton_, &QPushButton::clicked, this, [this] { startQueue(); });
|
||||
connect(stopButton_, &QPushButton::clicked, this, [this] { stopQueue(); });
|
||||
connect(exportButton_, &QPushButton::clicked, this, [this] { exportReport(); });
|
||||
connect(taskTable_, &QTableWidget::itemSelectionChanged, this, [this] { updateDetailsPanel(); });
|
||||
}
|
||||
|
||||
void MainWindow::addFiles() {
|
||||
const QStringList files = QFileDialog::getOpenFileNames(
|
||||
this,
|
||||
"Select audio files",
|
||||
QString(),
|
||||
"Audio Files (*.wav *.flac *.ape *.wv *.tak *.tta *.m4a *.alac *.shn *.ofr *.ofs *.mp3 *.aac);;All Files (*)");
|
||||
|
||||
addInputFiles(files);
|
||||
}
|
||||
|
||||
void MainWindow::addFolder() {
|
||||
const QString folder = QFileDialog::getExistingDirectory(this, "Select audio folder");
|
||||
if (folder.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList files;
|
||||
QDirIterator it(folder, kAudioNameFilters, QDir::Files, QDirIterator::Subdirectories);
|
||||
while (it.hasNext()) {
|
||||
files.append(it.next());
|
||||
}
|
||||
files.sort(Qt::CaseInsensitive);
|
||||
|
||||
if (files.isEmpty()) {
|
||||
QMessageBox::information(this, "No audio files", "No supported audio files were found in this folder.");
|
||||
return;
|
||||
}
|
||||
|
||||
addInputFiles(files);
|
||||
}
|
||||
|
||||
void MainWindow::addInputFiles(const QStringList &files) {
|
||||
if (files.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int added = 0;
|
||||
for (const QString &file : files) {
|
||||
const QString normalized = QFileInfo(file).absoluteFilePath();
|
||||
bool exists = false;
|
||||
for (const TaskItem &existing : tasks_) {
|
||||
if (existing.inputPath == normalized) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TaskItem task;
|
||||
task.inputPath = normalized;
|
||||
task.status = kStatusPending;
|
||||
task.conclusion = "-";
|
||||
tasks_.append(task);
|
||||
|
||||
const int row = taskTable_->rowCount();
|
||||
taskTable_->insertRow(row);
|
||||
taskTable_->setItem(row, 0, new QTableWidgetItem(normalized));
|
||||
taskTable_->setItem(row, 1, new QTableWidgetItem(task.status));
|
||||
taskTable_->setItem(row, 2, new QTableWidgetItem(task.conclusion));
|
||||
taskTable_->setItem(row, 3, new QTableWidgetItem(task.accuracy));
|
||||
taskTable_->setItem(row, 4, new QTableWidgetItem("-"));
|
||||
taskTable_->setItem(row, 5, new QTableWidgetItem("-"));
|
||||
++added;
|
||||
}
|
||||
|
||||
updateSummary();
|
||||
appendLog(QString("Added %1 file(s).").arg(added));
|
||||
}
|
||||
|
||||
void MainWindow::clearTasks() {
|
||||
if (running_) {
|
||||
QMessageBox::warning(this, "Busy", "Stop the run before clearing the queue.");
|
||||
return;
|
||||
}
|
||||
|
||||
tasks_.clear();
|
||||
taskTable_->setRowCount(0);
|
||||
updateSummary();
|
||||
updateDetailsPanel();
|
||||
appendLog("Queue cleared.");
|
||||
}
|
||||
|
||||
void MainWindow::exportReport() {
|
||||
QString path = reportPathEdit_->text().trimmed();
|
||||
if (path.isEmpty()) {
|
||||
path = QFileDialog::getSaveFileName(this, "Export report", "aucdtect-report.txt", "Text Files (*.txt)");
|
||||
}
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QMessageBox::critical(this, "Export failed", QString("Cannot open %1").arg(path));
|
||||
return;
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
stream << composeReport();
|
||||
file.close();
|
||||
reportPathEdit_->setText(path);
|
||||
appendLog(QString("Report saved to %1").arg(path));
|
||||
}
|
||||
|
||||
void MainWindow::startQueue() {
|
||||
if (tasks_.isEmpty()) {
|
||||
QMessageBox::information(this, "No tasks", "Add at least one audio file.");
|
||||
return;
|
||||
}
|
||||
if (running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopping_ = false;
|
||||
running_ = true;
|
||||
for (TaskItem &task : tasks_) {
|
||||
if (task.status == kStatusFailed || task.status == kStatusStopped || task.status == kStatusDone) {
|
||||
task.status = kStatusPending;
|
||||
task.conclusion = "-";
|
||||
task.accuracy = "-";
|
||||
task.fileHash.clear();
|
||||
task.signature.clear();
|
||||
task.analyzerOutput.clear();
|
||||
task.exitCode = -1;
|
||||
}
|
||||
}
|
||||
for (int row = 0; row < tasks_.size(); ++row) {
|
||||
refreshRow(row);
|
||||
}
|
||||
|
||||
setUiBusy(true);
|
||||
updateSummary();
|
||||
appendLog(QString("Queue started with %1 worker(s).").arg(parallelismSpin_->value()));
|
||||
scheduleWorkers();
|
||||
}
|
||||
|
||||
void MainWindow::stopQueue() {
|
||||
stopping_ = true;
|
||||
appendLog("Stop requested. Running workers will finish, pending tasks will be marked as stopped.");
|
||||
for (TaskItem &task : tasks_) {
|
||||
if (task.status == kStatusPending) {
|
||||
task.status = kStatusStopped;
|
||||
task.conclusion = "Cancelled before start";
|
||||
}
|
||||
}
|
||||
for (int row = 0; row < tasks_.size(); ++row) {
|
||||
refreshRow(row);
|
||||
}
|
||||
updateSummary();
|
||||
finishRunIfIdle();
|
||||
}
|
||||
|
||||
void MainWindow::scheduleWorkers() {
|
||||
if (!running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (!stopping_ && watchers_.size() < parallelismSpin_->value()) {
|
||||
int nextIndex = -1;
|
||||
for (int i = 0; i < tasks_.size(); ++i) {
|
||||
if (tasks_[i].status == kStatusPending && !watchers_.contains(i)) {
|
||||
nextIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextIndex < 0) {
|
||||
break;
|
||||
}
|
||||
startWorker(nextIndex);
|
||||
}
|
||||
|
||||
finishRunIfIdle();
|
||||
}
|
||||
|
||||
void MainWindow::startWorker(int taskIndex) {
|
||||
TaskItem &task = tasks_[taskIndex];
|
||||
task.status = kStatusRunning;
|
||||
task.conclusion = "Analyzing";
|
||||
task.accuracy = "-";
|
||||
refreshRow(taskIndex);
|
||||
updateSummary();
|
||||
|
||||
WorkerPayload payload;
|
||||
payload.taskIndex = taskIndex;
|
||||
payload.inputPath = task.inputPath;
|
||||
payload.decoderTemplate = decoderCommandEdit_->text().trimmed();
|
||||
payload.reportPath = reportPathEdit_->text().trimmed();
|
||||
|
||||
appendLog(QString("Worker started for %1").arg(task.inputPath));
|
||||
|
||||
auto *watcher = new QFutureWatcher<WorkerResult>(this);
|
||||
watchers_.insert(taskIndex, watcher);
|
||||
connect(watcher, &QFutureWatcher<WorkerResult>::finished, this, [this, taskIndex, watcher] {
|
||||
const WorkerResult result = watcher->result();
|
||||
watchers_.remove(taskIndex);
|
||||
watcher->deleteLater();
|
||||
applyWorkerResult(taskIndex, result);
|
||||
scheduleWorkers();
|
||||
});
|
||||
watcher->setFuture(QtConcurrent::run(runWorker, payload));
|
||||
}
|
||||
|
||||
void MainWindow::applyWorkerResult(int taskIndex, const WorkerResult &result) {
|
||||
if (taskIndex < 0 || taskIndex >= tasks_.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
TaskItem &task = tasks_[taskIndex];
|
||||
task.status = result.status;
|
||||
task.conclusion = result.conclusion;
|
||||
task.accuracy = result.accuracy.isEmpty() ? "-" : result.accuracy;
|
||||
task.fileHash = result.fileHash;
|
||||
task.signature = result.signature;
|
||||
task.analyzerOutput = result.analyzerOutput;
|
||||
task.exitCode = result.exitCode;
|
||||
|
||||
refreshRow(taskIndex);
|
||||
updateSummary();
|
||||
updateDetailsPanel();
|
||||
appendLog(QString("[%1] %2").arg(task.status, task.inputPath));
|
||||
}
|
||||
|
||||
void MainWindow::finishRunIfIdle() {
|
||||
if (!running_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!watchers_.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasPending = false;
|
||||
bool hasRunning = false;
|
||||
for (const TaskItem &task : tasks_) {
|
||||
hasPending |= (task.status == kStatusPending);
|
||||
hasRunning |= (task.status == kStatusRunning);
|
||||
}
|
||||
|
||||
if (hasPending || hasRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
running_ = false;
|
||||
setUiBusy(false);
|
||||
appendLog(stopping_ ? "Queue stopped." : "Queue finished.");
|
||||
}
|
||||
|
||||
void MainWindow::appendLog(const QString &message) {
|
||||
const QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
|
||||
logView_->appendPlainText(QString("[%1] %2").arg(timestamp, message));
|
||||
}
|
||||
|
||||
void MainWindow::setUiBusy(bool busy) {
|
||||
addFilesButton_->setEnabled(!busy);
|
||||
addFolderButton_->setEnabled(!busy);
|
||||
clearButton_->setEnabled(!busy);
|
||||
startButton_->setEnabled(!busy);
|
||||
stopButton_->setEnabled(busy);
|
||||
}
|
||||
|
||||
void MainWindow::refreshRow(int row) {
|
||||
if (row < 0 || row >= tasks_.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const TaskItem &task = tasks_[row];
|
||||
taskTable_->item(row, 1)->setText(task.status);
|
||||
taskTable_->item(row, 2)->setText(task.conclusion);
|
||||
taskTable_->item(row, 3)->setText(task.accuracy);
|
||||
taskTable_->item(row, 4)->setText(task.fileHash.isEmpty() ? "-" : task.fileHash);
|
||||
taskTable_->item(row, 5)->setText(task.exitCode >= 0 ? QString::number(task.exitCode) : "-");
|
||||
}
|
||||
|
||||
void MainWindow::updateSummary() {
|
||||
int pending = 0;
|
||||
int running = 0;
|
||||
int done = 0;
|
||||
int failed = 0;
|
||||
|
||||
for (const TaskItem &task : tasks_) {
|
||||
if (task.status == kStatusPending) {
|
||||
++pending;
|
||||
} else if (task.status == kStatusRunning) {
|
||||
++running;
|
||||
} else if (task.status == kStatusDone) {
|
||||
++done;
|
||||
} else if (task.status == kStatusFailed || task.status == kStatusStopped) {
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
|
||||
pendingLabel_->setText(QString("Pending: %1").arg(pending));
|
||||
runningLabel_->setText(QString("Running: %1").arg(running));
|
||||
doneLabel_->setText(QString("Done: %1").arg(done));
|
||||
failedLabel_->setText(QString("Failed: %1").arg(failed));
|
||||
}
|
||||
|
||||
void MainWindow::updateDetailsPanel() {
|
||||
const auto selected = taskTable_->selectionModel() ? taskTable_->selectionModel()->selectedRows() : QModelIndexList{};
|
||||
if (selected.isEmpty()) {
|
||||
detailsView_->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const int row = selected.first().row();
|
||||
if (row < 0 || row >= tasks_.size()) {
|
||||
detailsView_->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const TaskItem &task = tasks_[row];
|
||||
QString details;
|
||||
QTextStream stream(&details);
|
||||
stream << "File : " << task.inputPath << "\n";
|
||||
stream << "Status : " << task.status << "\n";
|
||||
stream << "Conclusion : " << task.conclusion << "\n";
|
||||
stream << "Accuracy : " << task.accuracy << "\n";
|
||||
stream << "Hash : " << (task.fileHash.isEmpty() ? "-" : task.fileHash) << "\n";
|
||||
stream << "Signature : " << (task.signature.isEmpty() ? "-" : task.signature) << "\n";
|
||||
stream << "Exit code : " << task.exitCode << "\n\n";
|
||||
stream << (task.analyzerOutput.isEmpty() ? "No detailed report yet." : task.analyzerOutput.trimmed());
|
||||
detailsView_->setPlainText(details);
|
||||
}
|
||||
|
||||
QString MainWindow::substitutePlaceholders(const QString &templateText, const TaskItem &task) const {
|
||||
QString result = templateText;
|
||||
result.replace("{input}", escapeShellArgument(task.inputPath));
|
||||
result.replace("{decoded}", escapeShellArgument(task.decodedPath));
|
||||
|
||||
QString reportPath = reportPathEdit_->text().trimmed();
|
||||
if (reportPath.isEmpty()) {
|
||||
reportPath = QDir(task.tempDirPath).filePath("analysis-report.txt");
|
||||
}
|
||||
result.replace("{report}", escapeShellArgument(reportPath));
|
||||
return result;
|
||||
}
|
||||
|
||||
QString MainWindow::escapeShellArgument(const QString &value) const {
|
||||
QString escaped = value;
|
||||
escaped.replace('\'', "'\"'\"'");
|
||||
return "'" + escaped + "'";
|
||||
}
|
||||
|
||||
QString MainWindow::composeReport() const {
|
||||
QString report;
|
||||
QTextStream stream(&report);
|
||||
stream << "auCDtect Linux Report\n";
|
||||
stream << "Date: " << QDateTime::currentDateTime().toString(Qt::ISODate) << "\n";
|
||||
stream << "Workers: " << parallelismSpin_->value() << "\n\n";
|
||||
|
||||
for (const TaskItem &task : tasks_) {
|
||||
stream << "FILE: " << task.inputPath << "\n";
|
||||
stream << "Accuracy : " << task.accuracy << "\n";
|
||||
stream << "Conclusion : " << task.conclusion << "\n";
|
||||
stream << "Status : " << task.status << "\n";
|
||||
stream << "Hash : " << (task.fileHash.isEmpty() ? "-" : task.fileHash) << "\n";
|
||||
stream << "Signature : " << (task.signature.isEmpty() ? "-" : task.signature) << "\n";
|
||||
stream << "Exit code : " << task.exitCode << "\n";
|
||||
if (!task.analyzerOutput.isEmpty()) {
|
||||
stream << "\n" << task.analyzerOutput.trimmed() << "\n";
|
||||
}
|
||||
stream << "\n";
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
Reference in New Issue
Block a user