From 8dbb96f3b6be46864f49a7b4fe6e06f82f2a59c9 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Mon, 18 Jan 2021 02:22:16 +0100 Subject: [PATCH 01/36] Add Grantlee lib to dependencies --- CMakeLists.txt | 10 +++- cmake/modules/FindGrantlee.cmake | 86 ++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 cmake/modules/FindGrantlee.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a236f3961ad..cea4a31f5acc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -594,7 +594,6 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/library/dlgtrackmetadataexport.cpp src/library/export/dlgtrackexport.ui src/library/export/trackexportdlg.cpp - src/library/export/trackexportwizard.cpp src/library/export/trackexportworker.cpp src/library/externaltrackcollection.cpp src/library/hiddentablemodel.cpp @@ -809,6 +808,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/util/duration.cpp src/util/experiment.cpp src/util/file.cpp + src/util/formatter.cpp src/util/imageutils.cpp src/util/indexrange.cpp src/util/logger.cpp @@ -1802,6 +1802,14 @@ target_link_libraries(mixxx-lib PUBLIC FpClassify) # in production code target_include_directories(mixxx-lib SYSTEM PUBLIC "${gtest_SOURCE_DIR}/include") +# FLAC +find_package(Grantlee5 REQUIRED) +#target_link_libraries(mixxx-lib PUBLIC Grantlee5::GRANTLEE) +target_link_libraries(mixxx-lib PUBLIC + Grantlee5::Templates + Grantlee5::TextDocument +) + # LAME find_package(LAME REQUIRED) target_link_libraries(mixxx-lib PUBLIC LAME::LAME) diff --git a/cmake/modules/FindGrantlee.cmake b/cmake/modules/FindGrantlee.cmake new file mode 100644 index 000000000000..8ef684c0e273 --- /dev/null +++ b/cmake/modules/FindGrantlee.cmake @@ -0,0 +1,86 @@ +# This file is part of Mixxx, Digital DJ'ing software. +# Copyright (C) 2001-2020 Mixxx Development Team +# Distributed under the GNU General Public Licence (GPL) version 2 or any later +# later version. See the LICENSE file for details. + +#[=======================================================================[.rst: +FindGRANTLEE +-------- + +Finds the GRANTLEE library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``GRANTLEE::GRANTLEE`` + The GRANTLEE library + +Result Variables +^^^^^^^^^^^^^^^^ + +This will define the following variables: + +``GRANTLEE_FOUND`` + True if the system has the GRANTLEE library. +``GRANTLEE_INCLUDE_DIRS`` + Include directories needed to use GRANTLEE. +``GRANTLEE_LIBRARIES`` + Libraries needed to link to GRANTLEE. +``GRANTLEE_DEFINITIONS`` + Compile definitions needed to use GRANTLEE. + +Cache Variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set: + +``GRANTLEE_INCLUDE_DIR`` + The directory containing ``GRANTLEE/all.h``. +``GRANTLEE_LIBRARY`` + The path to the GRANTLEE library. + +#]=======================================================================] + +find_package(PkgConfig QUIET) +if(PkgConfig_FOUND) + pkg_check_modules(PC_GRANTLEE QUIET Grantlee5) +endif() + +find_path(GRANTLEE_INCLUDE_DIR + NAMES grantlee/parser.h + PATHS ${PC_GRANTLEE_INCLUDE_DIRS} + DOC "Grantlee include directory") +mark_as_advanced(GRANTLEE_INCLUDE_DIR) + +find_library(GRANTLEE_LIBRARY + NAMES GRANTLEE + PATHS ${PC_GRANTLEE_LIBRARY_DIRS} + DOC "Grantlee library" +) +mark_as_advanced(GRANTLEE_LIBRARY) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + GRANTLEE + DEFAULT_MSG + GRANTLEE_LIBRARY + GRANTLEE_INCLUDE_DIR +) + +if(GRANTLEE_FOUND) + set(GRANTLEE_LIBRARIES "${GRANTLEE_LIBRARY}") + set(GRANTLEE_INCLUDE_DIRS "${GRANTLEE_INCLUDE_DIR}") + set(GRANTLEE_DEFINITIONS ${PC_GRANTLEE_CFLAGS_OTHER}) + + if(NOT TARGET GRANTLEE::GRANTLEE) + add_library(GRANTLEE::GRANTLEE UNKNOWN IMPORTED) + set_target_properties(GRANTLEE::GRANTLEE + PROPERTIES + IMPORTED_LOCATION "${GRANTLEE_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${PC_GRANTLEE_CFLAGS_OTHER}" + INTERFACE_INCLUDE_DIRECTORIES "${GRANTLEE_INCLUDE_DIR}" + ) + endif() +endif() From 5874dbeb5ddf892cc4191d633528c49a28065955 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Mon, 18 Jan 2021 02:22:47 +0100 Subject: [PATCH 02/36] Major track export overhaul * major rewrite of the export dialog * use a table to log all exported files and their errors * use template language to format the output destination * don't fail on errors, just log them * remove now obsolete export wizzard --- src/library/export/dlgtrackexport.ui | 278 +++++++++++++------ src/library/export/trackexportdlg.cpp | 214 ++++++++++++-- src/library/export/trackexportdlg.h | 32 ++- src/library/export/trackexportwizard.cpp | 35 --- src/library/export/trackexportwizard.h | 40 --- src/library/export/trackexportworker.cpp | 166 +++++++++-- src/library/export/trackexportworker.h | 61 +++- src/library/trackset/baseplaylistfeature.cpp | 14 +- src/library/trackset/crate/cratefeature.cpp | 15 +- src/library/trackset/crate/cratesummary.h | 11 +- src/test/trackexport_test.cpp | 43 ++- src/test/trackexport_test.h | 3 +- src/track/track.h | 24 ++ src/track/trackfile.h | 3 + src/util/formatter.cpp | 9 + src/util/formatter.h | 10 + 16 files changed, 722 insertions(+), 236 deletions(-) delete mode 100644 src/library/export/trackexportwizard.cpp delete mode 100644 src/library/export/trackexportwizard.h create mode 100644 src/util/formatter.cpp create mode 100644 src/util/formatter.h diff --git a/src/library/export/dlgtrackexport.ui b/src/library/export/dlgtrackexport.ui index 6f12538d8191..0c5afe761a70 100644 --- a/src/library/export/dlgtrackexport.ui +++ b/src/library/export/dlgtrackexport.ui @@ -6,8 +6,8 @@ 0 0 - 600 - 200 + 1425 + 958 @@ -31,105 +31,209 @@ Export Tracks + + false + + + + + Progress + + + + + + 24 + + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + QAbstractItemView::ExtendedSelection + + + false + + + + From + + + + + Destination + + + + + Result + + + + + + + - + true - + 0 0 - - Exporting Tracks - - - - - 10 - 26 - 561 - 150 - - - - - 0 - 0 - - - - - 6 - - - - - 10 - - - 0 - - - - - - - - - - (status text) - - - true - - - - - - - Qt::Vertical - - - - 20 - 20 - - - - - - - - 10 - - - + + + + + + + 0 + + + + + + 0 + 0 + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + true + + + + {{ track.fileName }} + + + - &Cancel + {{ track.artist }} - {{track.title }}.{{ track.extension }} - - - - - - Qt::Horizontal + + + + {{ track.bpm}} - {{ track.artist }} - {{track.title }}.{{ track.extension }} - - - 40 - 20 - + + + + {{ track.bpm}}/{{ track.artist }} - {{track.title }}.{{ track.extension }} - - - - - - + + + + + + + Pattern + + + + + + + Folder + + + + + + + + + + + + Browse + + + + + + + + + Preview + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + + &Start + + + + + + + &Close + + + + + + diff --git a/src/library/export/trackexportdlg.cpp b/src/library/export/trackexportdlg.cpp index d6c6c0335e14..6721a83bbe02 100644 --- a/src/library/export/trackexportdlg.cpp +++ b/src/library/export/trackexportdlg.cpp @@ -1,34 +1,78 @@ #include "library/export/trackexportdlg.h" #include +#include #include #include +#include #include "moc_trackexportdlg.cpp" #include "util/assert.h" -TrackExportDlg::TrackExportDlg(QWidget *parent, - UserSettingsPointer pConfig, - TrackExportWorker* worker) +TrackExportDlg::TrackExportDlg(QWidget* parent, + UserSettingsPointer pConfig, + TrackPointerList& tracks, + Grantlee::Context* context) : QDialog(parent), Ui::DlgTrackExport(), m_pConfig(pConfig), - m_worker(worker) { + m_tracks(tracks), + m_worker(nullptr), + m_context(context) { setupUi(this); + + QString lastExportDirectory = m_pConfig->getValue( + ConfigKey("[Library]", "LastTrackCopyDirectory"), + QStandardPaths::writableLocation(QStandardPaths::MusicLocation)); + folderEdit->setText(lastExportDirectory); + + m_worker = new TrackExportWorker(folderEdit->text(), m_tracks); + connect(cancelButton, &QPushButton::clicked, this, &TrackExportDlg::cancelButtonClicked); + connect(startButton, + &QPushButton::clicked, + this, + &TrackExportDlg::slotStartExport); exportProgress->setMinimum(0); exportProgress->setMaximum(1); exportProgress->setValue(0); - statusLabel->setText(""); setModal(true); + QHeaderView* headerView = statusTable->horizontalHeader(); + headerView->resizeSection(0, 600); + headerView->resizeSection(1, 600); + headerView->resizeSection(2, 100); + headerView->setStretchLastSection(true); + statusTable->setHorizontalHeader(headerView); + + connect(browseButton, + &QAbstractButton::clicked, + this, + &TrackExportDlg::slotBrowseFolder); + connect(folderEdit, + &QLineEdit::textChanged, + [=](const QString& x) { + Q_UNUSED(x); + updatePreview(); + }); + connect(comboPattern, + &QComboBox::currentTextChanged, + [this](const QString& x) { + Q_UNUSED(x); + updatePreview(); + }); + connect(m_worker, &TrackExportWorker::progress, this, &TrackExportDlg::slotProgress); + connect(m_worker, + &TrackExportWorker::result, + this, + &TrackExportDlg::slotResult); connect(m_worker, &TrackExportWorker::askOverwriteMode, this, @@ -36,28 +80,136 @@ TrackExportDlg::TrackExportDlg(QWidget *parent, connect(m_worker, &TrackExportWorker::canceled, this, - &TrackExportDlg::cancelButtonClicked); + &TrackExportDlg::stopWorker); + + if (m_tracks.isEmpty()) { + QMessageBox::warning( + nullptr, + tr("Export Error"), + tr("No files selected"), + QMessageBox::Ok, + QMessageBox::Ok); + hide(); + accept(); + } + updatePreview(); +} + +TrackExportDlg::~TrackExportDlg() { + if (m_context) { + delete m_context; + } +} + +bool TrackExportDlg::browseFolder() { + QString destDir = QFileDialog::getExistingDirectory( + nullptr, tr("Export Track Files To"), folderEdit->text()); + if (destDir.isEmpty()) { + return false; + } + folderEdit->setText(destDir); + return true; +} +void TrackExportDlg::closeEvent(QCloseEvent* event) { + if (m_worker->isRunning()) { + stopWorker(); + } + QDialog::closeEvent(event); +} + +void TrackExportDlg::setEnableControls(bool enabled) { + startButton->setEnabled(enabled); + comboPattern->setEnabled(enabled); + folderEdit->setEnabled(enabled); + browseButton->setEnabled(enabled); } -void TrackExportDlg::showEvent(QShowEvent* event) { - QDialog::showEvent(event); - VERIFY_OR_DEBUG_ASSERT(m_worker) { - // It's not worth checking for m_exporter != nullptr elsewhere in this - // class... it'll be clear very quickly that someone screwed up and - // forgot to call selectDestinationDirectory(). - qDebug() << "Programming error: did not initialize m_exporter, about to crash"; +void TrackExportDlg::slotStartExport() { + VERIFY_OR_DEBUG_ASSERT(m_worker->isRunning() == false) { + qWarning() << "Export already running"; return; } + // Do we want to check for target folder to exist ? + // When I file is exported, all parent folders are created automatically. + + // QString destDir = folderEdit->text(); + // if (!QFile::exists(destDir)) { + // if (!browseFolder()) { + // return; + // } + // destDir = folderEdit->text(); + // } + m_errorCount = 0; + m_okCount = 0; + m_skippedCount = 0; + + m_pConfig->setValue( + ConfigKey("[Library]", "LastTrackCopyDirectory"), + folderEdit->text()); + + // sets destDirectory and Pattern + updatePreview(); + setEnableControls(false); + cancelButton->setText(tr("&Cancel")); + m_worker->start(); } -void TrackExportDlg::slotProgress(const QString& filename, int progress, int count) { - if (progress == count) { - statusLabel->setText(tr("Export finished")); +void TrackExportDlg::updatePreview() { + VERIFY_OR_DEBUG_ASSERT(!m_tracks.isEmpty()) { + return; + } + QString pattern = comboPattern->currentText(); + m_worker->setPattern(&pattern); + m_worker->setDestDir(folderEdit->text()); + previewLabel->setText(m_worker->applyPattern(m_tracks[0], 0)); + errorLabel->setText(m_worker->errorMessage()); +} + +int TrackExportDlg::addStatus(const QString status, const QString to) { + auto scrollbar = statusTable->verticalScrollBar(); + bool atEnd = scrollbar->value() == scrollbar->maximum(); + int row = statusTable->rowCount(); + statusTable->setRowCount(row + 1); + auto item = new QTableWidgetItem(status); + statusTable->setItem(row, 0, item); + if (atEnd) { + statusTable->scrollToBottom(); + } + if (!to.isEmpty()) { + auto toItem = new QTableWidgetItem(to); + statusTable->setItem(row, 1, toItem); + } + return row; +} + +void TrackExportDlg::slotResult(TrackExportWorker::ExportResult result, const QString msg) { + int type = QTableWidgetItem::UserType; + if (result == TrackExportWorker::ExportResult::EXPORT_COMPLETE) { + exportProgress->setValue(exportProgress->maximum()); + addStatus(QString(tr("Export finished. %1 ok. %2 errors. %3 skipped")) + .arg(m_okCount) + .arg(m_errorCount) + .arg(m_skippedCount), + nullptr); finish(); - } else { - statusLabel->setText(tr("Exporting %1").arg(filename)); + return; + } else if (result == TrackExportWorker::ExportResult::OK) { + m_okCount++; + } else if (result == TrackExportWorker::ExportResult::SKIPPED) { + m_skippedCount++; + type = QTableWidgetItem::UserType + 2; + } else if (result == TrackExportWorker::ExportResult::FAILED) { + m_errorCount++; + type = QTableWidgetItem::UserType + 3; } + auto item = new QTableWidgetItem(msg, type); + int row = statusTable->rowCount() - 1; + statusTable->setItem(row, 2, item); +} + +void TrackExportDlg::slotProgress(const QString from, const QString to, int progress, int count) { + addStatus(from, to); exportProgress->setMinimum(0); exportProgress->setMaximum(count); exportProgress->setValue(progress); @@ -98,20 +250,22 @@ void TrackExportDlg::slotAskOverwriteMode( } void TrackExportDlg::cancelButtonClicked() { - finish(); + // we cancel the worker if running, otherwise we close the window + if (m_worker->isRunning()) { + stopWorker(); + } else { + hide(); + accept(); + } } -void TrackExportDlg::finish() { +void TrackExportDlg::stopWorker() { m_worker->stop(); m_worker->wait(); - if (m_worker->errorMessage().length()) { - QMessageBox::warning( - nullptr, - tr("Export Error"), - m_worker->errorMessage(), - QMessageBox::Ok, - QMessageBox::Ok); - } - hide(); - accept(); + setEnableControls(true); + cancelButton->setText(tr("&Close")); +} + +void TrackExportDlg::finish() { + stopWorker(); } diff --git a/src/library/export/trackexportdlg.h b/src/library/export/trackexportdlg.h index f4b2776aeb8e..12c1a0535e6e 100644 --- a/src/library/export/trackexportdlg.h +++ b/src/library/export/trackexportdlg.h @@ -1,5 +1,8 @@ #pragma once +#include + +#include #include #include #include @@ -23,29 +26,42 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { // The dialog is prepared, but not shown on construction. Does not // take ownership of the export worker. - TrackExportDlg(QWidget *parent, UserSettingsPointer pConfig, - TrackExportWorker* worker); - virtual ~TrackExportDlg() { } + TrackExportDlg(QWidget* parent, + UserSettingsPointer pConfig, + TrackPointerList& tracks, + Grantlee::Context* context = nullptr); + virtual ~TrackExportDlg(); public slots: - void slotProgress(const QString& filename, int progress, int count); + void slotProgress(const QString from, const QString to, int progress, int count); + void slotResult(TrackExportWorker::ExportResult result, const QString msg); void slotAskOverwriteMode( const QString& filename, std::promise* promise); void cancelButtonClicked(); + void slotBrowseFolder() { + browseFolder(); + }; + void slotStartExport(); protected: - // First pops up a directory selector on show(), then does the actual - // copying. - void showEvent(QShowEvent* event) override; + bool browseFolder(); private: // Called when progress is complete or the procedure has been canceled. - // Displays a final message box indicating success or failure. // Makes sure the exporter thread has exited. void finish(); + void stopWorker(); + int addStatus(const QString status, const QString to); + void updatePreview(); + void setEnableControls(bool enabled); + void closeEvent(QCloseEvent* event) override; UserSettingsPointer m_pConfig; TrackPointerList m_tracks; TrackExportWorker* m_worker; + Grantlee::Context* m_context; + int m_errorCount = 0; + int m_skippedCount = 0; + int m_okCount = 0; }; diff --git a/src/library/export/trackexportwizard.cpp b/src/library/export/trackexportwizard.cpp deleted file mode 100644 index 932b865b3f25..000000000000 --- a/src/library/export/trackexportwizard.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "library/export/trackexportwizard.h" - -#include -#include -#include -#include - -#include "moc_trackexportwizard.cpp" -#include "util/assert.h" - -void TrackExportWizard::exportTracks() { - if (!selectDestinationDirectory()) { - return; - } - // Ignore return value, upstream callers don't need it. - m_dialog->exec(); -} - -bool TrackExportWizard::selectDestinationDirectory() { - QString lastExportDirectory = m_pConfig->getValue( - ConfigKey("[Library]", "LastTrackCopyDirectory"), - QStandardPaths::writableLocation(QStandardPaths::MusicLocation)); - - QString destDir = QFileDialog::getExistingDirectory( - nullptr, tr("Export Track Files To"), lastExportDirectory); - if (destDir.isEmpty()) { - return false; - } - m_pConfig->set(ConfigKey("[Library]", "LastTrackCopyDirectory"), - ConfigValue(destDir)); - - m_worker.reset(new TrackExportWorker(destDir, m_tracks)); - m_dialog.reset(new TrackExportDlg(m_parent, m_pConfig, m_worker.data())); - return true; -} diff --git a/src/library/export/trackexportwizard.h b/src/library/export/trackexportwizard.h deleted file mode 100644 index 9954b8fa39f8..000000000000 --- a/src/library/export/trackexportwizard.h +++ /dev/null @@ -1,40 +0,0 @@ -// TrackExportWizard handles exporting a list of tracks to an external directory -// -// TODO: -// * Offer customizable file renaming -// * Offer the option to transcode files to the codec of choice (e.g., -// FLAC -> AIFF for CDJ -// * Export sidecar metadata files for import into Mixxx - -#pragma once - -#include -#include - -#include "library/export/trackexportdlg.h" -#include "library/export/trackexportworker.h" -#include "preferences/usersettings.h" -#include "track/track_decl.h" - -// A controller class for creating the export worker and UI. -class TrackExportWizard : public QObject { - Q_OBJECT - public: - TrackExportWizard(QWidget* parent, UserSettingsPointer pConfig, const TrackPointerList& tracks) - : m_parent(parent), m_pConfig(pConfig), m_tracks(tracks) { - } - virtual ~TrackExportWizard() { } - - // Displays a dialog requesting destination directory, then performs - // track export if a folder is chosen. Handles errors gracefully. - void exportTracks(); - - private: - bool selectDestinationDirectory(); - - QWidget* m_parent; - UserSettingsPointer m_pConfig; - TrackPointerList m_tracks; - QScopedPointer m_dialog; - QScopedPointer m_worker; -}; diff --git a/src/library/export/trackexportworker.cpp b/src/library/export/trackexportworker.cpp index dd9a16f56772..a5537b6ddcfd 100644 --- a/src/library/export/trackexportworker.cpp +++ b/src/library/export/trackexportworker.cpp @@ -1,14 +1,30 @@ #include "library/export/trackexportworker.h" +#include +#include +#include + #include #include #include +#include +#include +#include #include "moc_trackexportworker.cpp" #include "track/track.h" +#include "util/formatter.h" namespace { +const auto kBadFileCharacters = QRegularExpression( + QStringLiteral("\n"), QRegularExpression::MultilineOption); +const auto kResultSkipped = QStringLiteral("skipped"); +const auto kResultCanceled = QStringLiteral("Export canceled"); +const auto kResultEmptyPattern = QStringLiteral("empty pattern result, skipped"); +const auto kResultOk = QStringLiteral("ok"); +const auto kResultCantCreateDirectory = QStringLiteral("Could not create folder"); + QString rewriteFilename(const QFileInfo& fileinfo, int index) { // We don't have total control over the inputs, so definitely // don't use .arg().arg().arg(). @@ -16,17 +32,38 @@ QString rewriteFilename(const QFileInfo& fileinfo, int index) { return QString("%1-%2.%3").arg(fileinfo.baseName(), index_str, fileinfo.completeSuffix()); } +} // namespace + +TrackExportWorker::TrackExportWorker(const QString& destDir, + TrackPointerList& tracks, + QString* pattern, + Grantlee::Context* context) + : m_running(false), + m_destDir(destDir), + m_tracks(tracks), + m_context(context) { + qRegisterMetaType("TrackExportWorker::ExportResult"); + setPattern(pattern); +} + // Iterate over a list of tracks and generate a minimal set of files to copy. // Finds duplicate filenames. Munges filenames if they refer to different files, // and skips if they refer to the same disk location. Returns a map from // QString (the destination possibly-munged filenames) to QFileinfo (the source // file information). -QMap createCopylist(const TrackPointerList& tracks) { +// Tracks for which a empty filename was generated will be added to the skippedTracks +// list. +QMap TrackExportWorker::createCopylist(const TrackPointerList& tracks, + TrackPointerList* skippedTracks) { // QMap is a non-obvious return value, but it's easy for callers to use // in practice and is the best object for producing the final list // efficiently. QMap copylist; - for (const auto& it : tracks) { + for (auto& it : tracks) { + if (!it.get()) { + qWarning() << "nullptr in tracklist"; + continue; + } if (it->getCanonicalLocation().isEmpty()) { qWarning() << "File not found or inaccessible while exporting" @@ -40,7 +77,12 @@ QMap createCopylist(const TrackPointerList& tracks) { const auto trackFile = it->getFileInfo(); const auto fileName = trackFile.fileName(); - auto destFileName = fileName; + QString destFileName = generateFilename(it, 0); + if (destFileName.isEmpty()) { + //qWarning() << "pattern generated empty filename for:" << it; + skippedTracks->append(it); + continue; + } int duplicateCounter = 0; do { const auto duplicateIter = copylist.find(destFileName); @@ -63,31 +105,113 @@ QMap createCopylist(const TrackPointerList& tracks) { break; } // Next round - destFileName = rewriteFilename(trackFile.asFileInfo(), duplicateCounter); + destFileName = generateFilename(it, duplicateCounter); } while (!destFileName.isEmpty()); } return copylist; } -} // namespace +void TrackExportWorker::setPattern(QString* pattern) { + if (pattern == nullptr) { + m_pattern = nullptr; + if (!m_template.isNull()) { + m_template.reset(); + } + return; + } + if (!m_engine) { + m_engine = Formatter::getEngine(this); + m_engine->setSmartTrimEnabled(true); + } + m_pattern = pattern; + m_template = m_engine->newTemplate(*m_pattern, QStringLiteral("export")); + if (m_template->error()) { + m_errorMessage = m_template->errorString(); + } else { + m_errorMessage = QString(); + } +} void TrackExportWorker::run() { + m_running = true; + m_bStop = false; int i = 0; - QMap copy_list = createCopylist(m_tracks); + auto skippedTracks = TrackPointerList(); + QMap copy_list = createCopylist(m_tracks, &skippedTracks); + for (TrackPointer track : qAsConst(skippedTracks)) { + QString fileName = track->fileName(); + emit progress(fileName, nullptr, 0, copy_list.size()); + emit result(TrackExportWorker::ExportResult::SKIPPED, kResultEmptyPattern); + } + for (auto it = copy_list.constBegin(); it != copy_list.constEnd(); ++it) { // We emit progress twice per loop, which may seem excessive, but it // guarantees that we emit a sane progress before we start and after // we end. In between, each filename will get its own visible tick // on the bar, which looks really nice. - emit progress(it->fileName(), i, copy_list.size()); - copyFile((*it).asFileInfo(), it.key()); + QString fileName = it->fileName(); + QString target = it.key(); + emit progress(fileName, target, i, copy_list.size()); + copyFile((*it).asFileInfo(), target); if (atomicLoadAcquire(m_bStop)) { emit canceled(); + m_running = false; return; } ++i; - emit progress(it->fileName(), i, copy_list.size()); } + emit result(TrackExportWorker::ExportResult::EXPORT_COMPLETE, kResultOk); + m_running = false; +} + +// Returns the new filename for the track. Applies the pattern if set. +QString TrackExportWorker::generateFilename(TrackPointer track, int index) { + if (m_pattern) { + return applyPattern(track, index); + } + + const auto trackFile = track->getFileInfo(); + if (index == 0) { + return trackFile.fileName(); + } + return rewriteFilename(trackFile.asFileInfo(), index); +} + +// Applies the pattern on track +QString TrackExportWorker::applyPattern( + TrackPointer track, + int index) { + VERIFY_OR_DEBUG_ASSERT(!m_destDir.isEmpty()) { + qWarning() << "empty target directory"; + return QString(); + } + VERIFY_OR_DEBUG_ASSERT(!m_template.isNull()) { + qWarning() << "template missing"; + return QString(); + } + VERIFY_OR_DEBUG_ASSERT(m_engine) { + qWarning() << "engine missing"; + return QString(); + } + + Grantlee::Context* context = m_context; + if (context == nullptr) { + context = new Grantlee::Context(); + } + // fill the context with the proper variables + context->push(); + context->insert(QStringLiteral("directory"), m_destDir); + // this is safe since the context stack is popped after rendering + context->insert(QStringLiteral("track"), track.get()); + context->insert(QStringLiteral("index"), QVariant(index)); + + QString newName = m_template->render(context); + + // remove the context stack so it is clean again + context->pop(); + + // replace bad filename characters with spaces + return newName.replace(kBadFileCharacters, QStringLiteral(" ")); } void TrackExportWorker::copyFile(const QFileInfo& source_fileinfo, @@ -96,6 +220,11 @@ void TrackExportWorker::copyFile(const QFileInfo& source_fileinfo, const QString dest_path = QDir(m_destDir).filePath(dest_filename); QFileInfo dest_fileinfo(dest_path); + QDir destDir = dest_fileinfo.absoluteDir(); + if (!destDir.mkpath(destDir.absolutePath())) { + emit result(TrackExportWorker::ExportResult::FAILED, kResultCantCreateDirectory); + } + if (dest_fileinfo.exists()) { switch (m_overwriteMode) { // Give the user the option to overwrite existing files in the destination. @@ -104,18 +233,20 @@ void TrackExportWorker::copyFile(const QFileInfo& source_fileinfo, case OverwriteAnswer::SKIP: case OverwriteAnswer::SKIP_ALL: qDebug() << "skipping" << sourceFilename; + emit result(TrackExportWorker::ExportResult::SKIPPED, kResultSkipped); return; case OverwriteAnswer::OVERWRITE: case OverwriteAnswer::OVERWRITE_ALL: break; case OverwriteAnswer::CANCEL: - m_errorMessage = tr("Export process was canceled"); + emit result(TrackExportWorker::ExportResult::EXPORT_COMPLETE, kResultCanceled); stop(); return; } break; case OverwriteMode::SKIP_ALL: qDebug() << "skipping" << sourceFilename; + emit result(TrackExportWorker::ExportResult::SKIPPED, kResultSkipped); return; case OverwriteMode::OVERWRITE_ALL:; } @@ -125,11 +256,11 @@ void TrackExportWorker::copyFile(const QFileInfo& source_fileinfo, qDebug() << "Removing existing file" << dest_path; if (!dest_file.remove()) { const QString error_message = tr( - "Error removing file %1: %2. Stopping.").arg( - dest_path, dest_file.errorString()); + "Error removing file %1: %2") + .arg(dest_path, dest_file.errorString()); qWarning() << error_message; - m_errorMessage = error_message; - stop(); + auto msg = QString("Failed: %1").arg(error_message); + emit result(TrackExportWorker::ExportResult::FAILED, msg); return; } } @@ -141,10 +272,11 @@ void TrackExportWorker::copyFile(const QFileInfo& source_fileinfo, "Error exporting track %1 to %2: %3. Stopping.").arg( sourceFilename, dest_path, source_file.errorString()); qWarning() << error_message; - m_errorMessage = error_message; - stop(); + auto msg = QString("Failed: %1").arg(error_message); + emit result(TrackExportWorker::ExportResult::FAILED, msg); return; } + emit result(TrackExportWorker::ExportResult::OK, kResultOk); } TrackExportWorker::OverwriteAnswer TrackExportWorker::makeOverwriteRequest( @@ -168,7 +300,6 @@ TrackExportWorker::OverwriteAnswer TrackExportWorker::makeOverwriteRequest( if (!mode_future.valid()) { qWarning() << "TrackExportWorker::makeOverwriteRequest invalid answer from future"; - m_errorMessage = tr("Error exporting tracks"); stop(); return OverwriteAnswer::CANCEL; } @@ -183,7 +314,6 @@ TrackExportWorker::OverwriteAnswer TrackExportWorker::makeOverwriteRequest( break; case OverwriteAnswer::CANCEL: // Handle cancellation as a result of the question. - m_errorMessage = tr("Export process was canceled"); stop(); break; default:; diff --git a/src/library/export/trackexportworker.h b/src/library/export/trackexportworker.h index 353abc002ffe..0bedfe91b121 100644 --- a/src/library/export/trackexportworker.h +++ b/src/library/export/trackexportworker.h @@ -1,15 +1,21 @@ #pragma once +#include + #include #include #include #include #include -#include "track/track_decl.h" +#include "track/track.h" class QFileInfo; +namespace Grantlee { +class Context; +class Engine; +} // namespace Grantlee // A QThread class for copying a list of files to a single destination directory. // Currently does not preserve subdirectory relationships. This class performs // all copies in a blocking style within its own thread. May be canceled from @@ -17,6 +23,14 @@ class QFileInfo; class TrackExportWorker : public QThread { Q_OBJECT public: + enum ExportResult { + OK, + SKIPPED, + FAILED, + EXPORT_COMPLETE + }; + Q_ENUM(ExportResult) + enum class OverwriteMode { ASK, OVERWRITE_ALL, @@ -35,20 +49,44 @@ class TrackExportWorker : public QThread { // Constructor does not validate the destination directory. Calling classes // should do that. - TrackExportWorker(const QString& destDir, const TrackPointerList& tracks) - : m_destDir(destDir), m_tracks(tracks) { - } + // pattern will + TrackExportWorker(const QString& destDir, + TrackPointerList& tracks, + QString* pattern = nullptr, + Grantlee::Context* context = nullptr); + virtual ~TrackExportWorker() { }; // exports ALL the tracks. Thread joins on success or failure. void run() override; + bool isRunning() { + return m_running; + }; // Calling classes can call errorMessage after a failure for a user-friendly // message about what happened. QString errorMessage() const { return m_errorMessage; } + void setDestDir(const QString& destDir) { + m_destDir = destDir; + } + + /// Sets the filename pattern + void setPattern(QString* pattern); + + /// returns the current filename pattern + QString* getPattern() { + return m_pattern; + } + + // Returns the new filename for the track. Applies the pattern if set. + QString generateFilename(TrackPointer track, int index = 0); + + /// Applies the filename pattern on track + QString applyPattern(TrackPointer track, int index); + // Cancels the export after the current copy operation. // May be called from another thread. void stop(); @@ -62,7 +100,8 @@ class TrackExportWorker : public QThread { void askOverwriteMode( const QString& filename, std::promise* promise); - void progress(const QString& filename, int progress, int count); + void progress(const QString from, const QString to, int progress, int count); + void result(TrackExportWorker::ExportResult result, const QString msg); void canceled(); private: @@ -73,15 +112,23 @@ class TrackExportWorker : public QThread { // process entirely. void copyFile(const QFileInfo& source_fileinfo, const QString& dest_filename); + QMap createCopylist( + const TrackPointerList& tracks, + TrackPointerList* skippedTracks); // Emit a signal requesting overwrite mode, and block until we get an // answer. Updates m_overwriteMode appropriately. OverwriteAnswer makeOverwriteRequest(const QString& filename); QAtomicInt m_bStop = false; + bool m_running = false; QString m_errorMessage; OverwriteMode m_overwriteMode = OverwriteMode::ASK; - const QString m_destDir; - const TrackPointerList m_tracks; + QString m_destDir; + TrackPointerList m_tracks; + QString* m_pattern; + Grantlee::Context* m_context; + Grantlee::Template m_template; + Grantlee::Engine* m_engine{nullptr}; }; diff --git a/src/library/trackset/baseplaylistfeature.cpp b/src/library/trackset/baseplaylistfeature.cpp index 3f686e158e9d..a9b5665f72d3 100644 --- a/src/library/trackset/baseplaylistfeature.cpp +++ b/src/library/trackset/baseplaylistfeature.cpp @@ -6,7 +6,7 @@ #include #include "controllers/keyboard/keyboardeventfilter.h" -#include "library/export/trackexportwizard.h" +#include "library/export/trackexportdlg.h" #include "library/library.h" #include "library/parser.h" #include "library/parsercsv.h" @@ -615,8 +615,16 @@ void BasePlaylistFeature::slotExportTrackFiles() { tracks.push_back(pPlaylistTableModel->getTrack(index)); } - TrackExportWizard track_export(nullptr, m_pConfig, tracks); - track_export.exportTracks(); + Grantlee::Context* context = new Grantlee::Context(); + + // FIXME(poelzi): make CrateSummary a QObject so it can be inserted into the + // context. Why the linker errors when QObject ? + // auto summary = new CrateSummary(); + // m_pTrackCollection->crates().readCrateSummaryById(id, summary); + // context->insert(QStringLiteral("crate"), summary); + + auto exportDialog = new TrackExportDlg(nullptr, m_pConfig, tracks, context); + exportDialog->open(); } void BasePlaylistFeature::slotAddToAutoDJ() { diff --git a/src/library/trackset/crate/cratefeature.cpp b/src/library/trackset/crate/cratefeature.cpp index 2cd5f088b7f7..8e39906ed404 100644 --- a/src/library/trackset/crate/cratefeature.cpp +++ b/src/library/trackset/crate/cratefeature.cpp @@ -8,7 +8,7 @@ #include #include -#include "library/export/trackexportwizard.h" +#include "library/export/trackexportdlg.h" #include "library/library.h" #include "library/parser.h" #include "library/parsercsv.h" @@ -730,9 +730,10 @@ void CrateFeature::slotExportPlaylist() { void CrateFeature::slotExportTrackFiles() { // Create a new table model since the main one might have an active search. + CrateId id = crateIdFromIndex(m_lastRightClickedIndex); QScopedPointer pCrateTableModel( new CrateTableModel(this, m_pLibrary->trackCollections())); - pCrateTableModel->selectCrate(m_crateTableModel.selectedCrate()); + pCrateTableModel->selectCrate(id); pCrateTableModel->select(); int rows = pCrateTableModel->rowCount(); @@ -742,8 +743,14 @@ void CrateFeature::slotExportTrackFiles() { trackpointers.push_back(m_crateTableModel.getTrack(index)); } - TrackExportWizard track_export(nullptr, m_pConfig, trackpointers); - track_export.exportTracks(); + Grantlee::Context* context = new Grantlee::Context(); + + // auto summary = new CrateSummary(); + // m_pTrackCollection->crates().readCrateSummaryById(id, summary); + // context->insert(QStringLiteral("crate"), summary); + + auto exportDialog = new TrackExportDlg(nullptr, m_pConfig, trackpointers, context); + exportDialog->open(); } void CrateFeature::slotCrateTableChanged(CrateId crateId) { diff --git a/src/library/trackset/crate/cratesummary.h b/src/library/trackset/crate/cratesummary.h index 3e28d3f88ef4..8d6da188eaf8 100644 --- a/src/library/trackset/crate/cratesummary.h +++ b/src/library/trackset/crate/cratesummary.h @@ -1,17 +1,24 @@ #pragma once +#include + #include "library/trackset/crate/crate.h" #include "util/duration.h" // A crate with aggregated track properties (total count + duration) class CrateSummary : public Crate { + // Q_OBJECT public: - explicit CrateSummary(CrateId id = CrateId()) + // Q_PROPERTY(uint trackCount READ getTrackCount WRITE setTrackCount) + // Q_PROPERTY(double trackDuration READ getTrackDuration WRITE setTrackDuration) + // Q_PROPERTY(QString name READ getName WRITE setName) + + CrateSummary(CrateId id = CrateId()) : Crate(id), m_trackCount(0), m_trackDuration(0.0) { } - ~CrateSummary() override = default; + ~CrateSummary() override{}; // The number of all tracks in this crate uint getTrackCount() const { diff --git a/src/test/trackexport_test.cpp b/src/test/trackexport_test.cpp index d897eba5205c..5e940575bcdb 100644 --- a/src/test/trackexport_test.cpp +++ b/src/test/trackexport_test.cpp @@ -3,6 +3,8 @@ #include "test/trackexport_test.h" +#include + #include #include @@ -11,8 +13,12 @@ FakeOverwriteAnswerer::~FakeOverwriteAnswerer() { } -void FakeOverwriteAnswerer::slotProgress(const QString& filename, int progress, int count) { +void FakeOverwriteAnswerer::slotProgress( + const QString filename, const QString to, int progress, int count) { m_progress_filename = filename; + if (!to.isEmpty()) { + m_progress_to = to; + } m_progress = progress; m_progress_count = count; } @@ -300,3 +306,38 @@ TEST_F(TrackExporterTest, MungeFilename) { // Remove the track we created. tempPath.remove("cover-test.ogg"); } + +TEST_F(TrackExporterTest, PatternExport) { + // Create a simple list of trackpointers and export them. + TrackFile fileinfo1(m_testDataDir.filePath("cover-test.ogg")); + TrackPointer track1(Track::newTemporary(fileinfo1)); + TrackFile fileinfo2(m_testDataDir.filePath("cover-test.flac")); + TrackPointer track2(Track::newTemporary(fileinfo2)); + TrackFile fileinfo3(m_testDataDir.filePath("cover-test-itunes-12.3.0-aac.m4a")); + TrackPointer track3(Track::newTemporary(fileinfo3)); + + // An initializer list would be prettier here, but it doesn't compile + // on MSVC or OSX. + TrackPointerList tracks; + tracks.append(track1); + tracks.append(track2); + tracks.append(track3); + auto context = new Grantlee::Context(); + context->insert("t", "t42/"); + auto pattern = QStringLiteral( + "{{t}}{{track.baseName}}-{{track.extension}}-" + "{{track.bpm}}{% if index %}-{{index}}{%endif%}"); + TrackExportWorker worker(m_exportDir.canonicalPath(), tracks, &pattern, context); + + EXPECT_EQ(worker.generateFilename(track1, 0), + QStringLiteral("t42/cover-test-ogg-0")); + EXPECT_EQ(worker.generateFilename(track2, 1), + QStringLiteral("t42/cover-test-flac-0-1")); + EXPECT_EQ(worker.generateFilename(track3, 0), + QStringLiteral("t42/cover-test-itunes-12-m4a-0")); + + auto pattern2 = QStringLiteral("{{track.fileName}}"); + worker.setPattern(&pattern2); + EXPECT_EQ(worker.generateFilename(track1, 0), + QStringLiteral("cover-test.ogg")); +} diff --git a/src/test/trackexport_test.h b/src/test/trackexport_test.h index 00a543bf6aeb..cd38b29b689e 100644 --- a/src/test/trackexport_test.h +++ b/src/test/trackexport_test.h @@ -48,7 +48,7 @@ class FakeOverwriteAnswerer : public QObject { } public slots: - void slotProgress(const QString& filename, int progress, int count); + void slotProgress(const QString status, const QString to, int progress, int count); void slotAskOverwriteMode( const QString& filename, std::promise* promise); @@ -58,6 +58,7 @@ class FakeOverwriteAnswerer : public QObject { TrackExportWorker* m_worker; QMap m_answers; QString m_progress_filename; + QString m_progress_to; int m_progress = 0; int m_progress_count = 0; }; diff --git a/src/track/track.h b/src/track/track.h index 5c6f70d65445..cefc3baff54e 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -52,7 +52,9 @@ class Track : public QObject { Q_PROPERTY(QString track_number READ getTrackNumber WRITE setTrackNumber) Q_PROPERTY(QString track_total READ getTrackTotal WRITE setTrackTotal) Q_PROPERTY(int times_played READ getTimesPlayed) + Q_PROPERTY(QDateTime lastPlayed READ getLastPlayedAt) Q_PROPERTY(QString comment READ getComment WRITE setComment) + Q_PROPERTY(int bitrate READ getBitrate WRITE setBitrate) Q_PROPERTY(double bpm READ getBpm WRITE setBpm) Q_PROPERTY(QString bpmFormatted READ getBpmText STORED false) Q_PROPERTY(QString key READ getKeyText WRITE setKeyText) @@ -62,6 +64,14 @@ class Track : public QObject { Q_PROPERTY(QString durationFormattedMilliseconds READ getDurationTextMilliseconds STORED false) Q_PROPERTY(QString info READ getInfo STORED false) Q_PROPERTY(QString titleInfo READ getTitleInfo STORED false) + Q_PROPERTY(int rating READ getRating WRITE setRating) + + Q_PROPERTY(QString location READ getLocation STORED false) + Q_PROPERTY(QString directory READ directory STORED false) + Q_PROPERTY(QString baseName READ baseName STORED false) + Q_PROPERTY(QString fileName READ fileName STORED false) + Q_PROPERTY(QString extension READ extension STORED false) + Q_PROPERTY(QString url READ getURL WRITE setURL) TrackFile getFileInfo() const { // Copying TrackFile/QFileInfo is thread-safe (implicit sharing), no locking needed. @@ -78,6 +88,20 @@ class Track : public QObject { QString getLocation() const { return m_fileInfo.location(); } + + QString directory() const { + return m_fileInfo.directory(); + } + QString baseName() const { + return m_fileInfo.baseName(); + } + QString fileName() const { + return m_fileInfo.fileName(); + } + QString extension() const { + return m_fileInfo.extension(); + } + // The (refreshed) canonical location QString getCanonicalLocation() const; // Checks if the file exists diff --git a/src/track/trackfile.h b/src/track/trackfile.h index 7e037840a948..5ba332c13d76 100644 --- a/src/track/trackfile.h +++ b/src/track/trackfile.h @@ -61,6 +61,9 @@ class TrackFile { QString baseName() const { return m_fileInfo.baseName(); } + QString extension() const { + return m_fileInfo.suffix(); + } QString fileName() const { return m_fileInfo.fileName(); } diff --git a/src/util/formatter.cpp b/src/util/formatter.cpp new file mode 100644 index 000000000000..721b12c4e9b8 --- /dev/null +++ b/src/util/formatter.cpp @@ -0,0 +1,9 @@ +#include "util/formatter.h" + +#include + +Grantlee::Engine* Formatter::getEngine(QObject* parent) { + auto engine = new Grantlee::Engine(parent); + // register custom + return engine; +} diff --git a/src/util/formatter.h b/src/util/formatter.h new file mode 100644 index 000000000000..72938bde5370 --- /dev/null +++ b/src/util/formatter.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +#include + +class Formatter { + public: + static Grantlee::Engine* getEngine(QObject* parent); +}; From ad7ac5171caab4821bdb970f4838a03ddc0c7c39 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Mon, 18 Jan 2021 02:41:12 +0100 Subject: [PATCH 03/36] Use the the last right click selected playlist, not the current --- src/library/trackset/baseplaylistfeature.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/library/trackset/baseplaylistfeature.cpp b/src/library/trackset/baseplaylistfeature.cpp index a9b5665f72d3..16d7775b40b2 100644 --- a/src/library/trackset/baseplaylistfeature.cpp +++ b/src/library/trackset/baseplaylistfeature.cpp @@ -602,7 +602,7 @@ void BasePlaylistFeature::slotExportTrackFiles() { m_pLibrary->trackCollections(), "mixxx.db.model.playlist_export")); - pPlaylistTableModel->setTableModel(m_pPlaylistTableModel->getPlaylist()); + pPlaylistTableModel->setTableModel(playlistIdFromIndex(m_lastRightClickedIndex)); pPlaylistTableModel->setSort(pPlaylistTableModel->fieldIndex( ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION), Qt::AscendingOrder); From 167550a91e461e7065c785b29c70f3cd6c38d680 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Mon, 18 Jan 2021 10:07:58 +0100 Subject: [PATCH 04/36] Add libgrantlee5-dev to debian build env --- packaging/debian/control.in | 1 + tools/debian_buildenv.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/packaging/debian/control.in b/packaging/debian/control.in index da395c4659e5..f186d75be9ba 100644 --- a/packaging/debian/control.in +++ b/packaging/debian/control.in @@ -27,6 +27,7 @@ Build-Depends: debhelper (>= 11), # Only needed for running tests that use SQLite. libqt5sql5-sqlite, libqt5x11extras5-dev, + libgrantlee5-dev, cmake (>= 3.13), libjack-dev, portaudio19-dev, diff --git a/tools/debian_buildenv.sh b/tools/debian_buildenv.sh index e151a7973626..404963d66aa5 100755 --- a/tools/debian_buildenv.sh +++ b/tools/debian_buildenv.sh @@ -44,6 +44,7 @@ case "$COMMAND" in libfaad-dev \ libfftw3-dev \ libflac-dev \ + libgrantlee5-dev \ libhidapi-dev \ libid3tag0-dev \ liblilv-dev \ From 69f357db048040becc8cb9570abf7e2b91121a6b Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Mon, 18 Jan 2021 23:57:08 +0100 Subject: [PATCH 05/36] Fix build problems --- CMakeLists.txt | 4 +- cmake/modules/FindGrantlee.cmake | 86 -------------------------------- 2 files changed, 2 insertions(+), 88 deletions(-) delete mode 100644 cmake/modules/FindGrantlee.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index cea4a31f5acc..16b52f2570ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1802,9 +1802,9 @@ target_link_libraries(mixxx-lib PUBLIC FpClassify) # in production code target_include_directories(mixxx-lib SYSTEM PUBLIC "${gtest_SOURCE_DIR}/include") -# FLAC +# Grantlee5 find_package(Grantlee5 REQUIRED) -#target_link_libraries(mixxx-lib PUBLIC Grantlee5::GRANTLEE) + target_link_libraries(mixxx-lib PUBLIC Grantlee5::Templates Grantlee5::TextDocument diff --git a/cmake/modules/FindGrantlee.cmake b/cmake/modules/FindGrantlee.cmake deleted file mode 100644 index 8ef684c0e273..000000000000 --- a/cmake/modules/FindGrantlee.cmake +++ /dev/null @@ -1,86 +0,0 @@ -# This file is part of Mixxx, Digital DJ'ing software. -# Copyright (C) 2001-2020 Mixxx Development Team -# Distributed under the GNU General Public Licence (GPL) version 2 or any later -# later version. See the LICENSE file for details. - -#[=======================================================================[.rst: -FindGRANTLEE --------- - -Finds the GRANTLEE library. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module provides the following imported targets, if found: - -``GRANTLEE::GRANTLEE`` - The GRANTLEE library - -Result Variables -^^^^^^^^^^^^^^^^ - -This will define the following variables: - -``GRANTLEE_FOUND`` - True if the system has the GRANTLEE library. -``GRANTLEE_INCLUDE_DIRS`` - Include directories needed to use GRANTLEE. -``GRANTLEE_LIBRARIES`` - Libraries needed to link to GRANTLEE. -``GRANTLEE_DEFINITIONS`` - Compile definitions needed to use GRANTLEE. - -Cache Variables -^^^^^^^^^^^^^^^ - -The following cache variables may also be set: - -``GRANTLEE_INCLUDE_DIR`` - The directory containing ``GRANTLEE/all.h``. -``GRANTLEE_LIBRARY`` - The path to the GRANTLEE library. - -#]=======================================================================] - -find_package(PkgConfig QUIET) -if(PkgConfig_FOUND) - pkg_check_modules(PC_GRANTLEE QUIET Grantlee5) -endif() - -find_path(GRANTLEE_INCLUDE_DIR - NAMES grantlee/parser.h - PATHS ${PC_GRANTLEE_INCLUDE_DIRS} - DOC "Grantlee include directory") -mark_as_advanced(GRANTLEE_INCLUDE_DIR) - -find_library(GRANTLEE_LIBRARY - NAMES GRANTLEE - PATHS ${PC_GRANTLEE_LIBRARY_DIRS} - DOC "Grantlee library" -) -mark_as_advanced(GRANTLEE_LIBRARY) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - GRANTLEE - DEFAULT_MSG - GRANTLEE_LIBRARY - GRANTLEE_INCLUDE_DIR -) - -if(GRANTLEE_FOUND) - set(GRANTLEE_LIBRARIES "${GRANTLEE_LIBRARY}") - set(GRANTLEE_INCLUDE_DIRS "${GRANTLEE_INCLUDE_DIR}") - set(GRANTLEE_DEFINITIONS ${PC_GRANTLEE_CFLAGS_OTHER}) - - if(NOT TARGET GRANTLEE::GRANTLEE) - add_library(GRANTLEE::GRANTLEE UNKNOWN IMPORTED) - set_target_properties(GRANTLEE::GRANTLEE - PROPERTIES - IMPORTED_LOCATION "${GRANTLEE_LIBRARY}" - INTERFACE_COMPILE_OPTIONS "${PC_GRANTLEE_CFLAGS_OTHER}" - INTERFACE_INCLUDE_DIRECTORIES "${GRANTLEE_INCLUDE_DIR}" - ) - endif() -endif() From 4672c8bcf1af7f088a2efcfbfdae4f045323411e Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Tue, 19 Jan 2021 00:57:30 +0100 Subject: [PATCH 06/36] Make Key a Q_GADGET and add string properties for formatting --- src/track/keys.cpp | 14 +++++++++++++- src/track/keys.h | 13 +++++++++++++ src/track/track.h | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/track/keys.cpp b/src/track/keys.cpp index 161694abc60a..989eb08dbbc1 100644 --- a/src/track/keys.cpp +++ b/src/track/keys.cpp @@ -1,7 +1,9 @@ +#include "track/keys.h" + #include #include -#include "track/keys.h" +#include "track/keyutils.h" using mixxx::track::io::key::ChromaticKey; using mixxx::track::io::key::KeyMap; @@ -16,6 +18,16 @@ Keys::Keys(const KeyMap& keyMap) : m_keyMap(keyMap) { } +QString Keys::getTraditional() const { + return KeyUtils::getGlobalKeyText(*this, KeyUtils::KeyNotation::Traditional); +} +QString Keys::getOpenkey() const { + return KeyUtils::getGlobalKeyText(*this, KeyUtils::KeyNotation::OpenKey); +} +QString Keys::getLancelot() const { + return KeyUtils::getGlobalKeyText(*this, KeyUtils::KeyNotation::Lancelot); +} + QByteArray Keys::toByteArray() const { std::string output; m_keyMap.SerializeToString(&output); diff --git a/src/track/keys.h b/src/track/keys.h index ddc0b414c164..499fa46968cf 100644 --- a/src/track/keys.h +++ b/src/track/keys.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -13,9 +14,19 @@ typedef QVector > KeyChangeLi class KeyFactory; class Keys final { + Q_GADGET public: + Q_PROPERTY(QString traditional READ getTraditional) + Q_PROPERTY(QString openkey READ getOpenkey) + Q_PROPERTY(QString lancelot READ getLancelot) + Q_PROPERTY(bool isValid READ isValid) + explicit Keys(const QByteArray* pByteArray = nullptr); + QString getTraditional() const; + QString getOpenkey() const; + QString getLancelot() const; + // Serialization QByteArray toByteArray() const; @@ -52,3 +63,5 @@ class Keys final { // For private constructor access. friend class KeyFactory; }; + +Q_DECLARE_METATYPE(Keys) diff --git a/src/track/track.h b/src/track/track.h index cefc3baff54e..535589832fe7 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -57,7 +57,7 @@ class Track : public QObject { Q_PROPERTY(int bitrate READ getBitrate WRITE setBitrate) Q_PROPERTY(double bpm READ getBpm WRITE setBpm) Q_PROPERTY(QString bpmFormatted READ getBpmText STORED false) - Q_PROPERTY(QString key READ getKeyText WRITE setKeyText) + Q_PROPERTY(Keys key READ getKeys WRITE setKeys) Q_PROPERTY(double duration READ getDuration) Q_PROPERTY(QString durationFormatted READ getDurationTextSeconds STORED false) Q_PROPERTY(QString durationFormattedCentiseconds READ getDurationTextCentiseconds STORED false) From b97b6cc6177e63f2b8b9194dafb3959ef3b4346f Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Tue, 19 Jan 2021 01:11:47 +0100 Subject: [PATCH 07/36] Use index for position index and dup for duplication counter --- src/library/export/trackexportworker.cpp | 19 ++++++++++++------- src/library/export/trackexportworker.h | 4 ++-- src/test/trackexport_test.cpp | 10 +++++----- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/library/export/trackexportworker.cpp b/src/library/export/trackexportworker.cpp index a5537b6ddcfd..7ee0f1a0e56d 100644 --- a/src/library/export/trackexportworker.cpp +++ b/src/library/export/trackexportworker.cpp @@ -59,7 +59,9 @@ QMap TrackExportWorker::createCopylist(const TrackPointerLis // in practice and is the best object for producing the final list // efficiently. QMap copylist; + int index = 0; for (auto& it : tracks) { + index++; if (!it.get()) { qWarning() << "nullptr in tracklist"; continue; @@ -77,7 +79,7 @@ QMap TrackExportWorker::createCopylist(const TrackPointerLis const auto trackFile = it->getFileInfo(); const auto fileName = trackFile.fileName(); - QString destFileName = generateFilename(it, 0); + QString destFileName = generateFilename(it, index, 0); if (destFileName.isEmpty()) { //qWarning() << "pattern generated empty filename for:" << it; skippedTracks->append(it); @@ -105,7 +107,7 @@ QMap TrackExportWorker::createCopylist(const TrackPointerLis break; } // Next round - destFileName = generateFilename(it, duplicateCounter); + destFileName = generateFilename(it, index, duplicateCounter); } while (!destFileName.isEmpty()); } return copylist; @@ -160,27 +162,29 @@ void TrackExportWorker::run() { } ++i; } + emit progress(QStringLiteral(""), QStringLiteral(""), copy_list.size(), copy_list.size()); emit result(TrackExportWorker::ExportResult::EXPORT_COMPLETE, kResultOk); m_running = false; } // Returns the new filename for the track. Applies the pattern if set. -QString TrackExportWorker::generateFilename(TrackPointer track, int index) { +QString TrackExportWorker::generateFilename(TrackPointer track, int index, int dupCounter) { if (m_pattern) { - return applyPattern(track, index); + return applyPattern(track, index, dupCounter); } const auto trackFile = track->getFileInfo(); - if (index == 0) { + if (dupCounter == 0) { return trackFile.fileName(); } - return rewriteFilename(trackFile.asFileInfo(), index); + return rewriteFilename(trackFile.asFileInfo(), dupCounter); } // Applies the pattern on track QString TrackExportWorker::applyPattern( TrackPointer track, - int index) { + int index, + int duplicateCounter) { VERIFY_OR_DEBUG_ASSERT(!m_destDir.isEmpty()) { qWarning() << "empty target directory"; return QString(); @@ -204,6 +208,7 @@ QString TrackExportWorker::applyPattern( // this is safe since the context stack is popped after rendering context->insert(QStringLiteral("track"), track.get()); context->insert(QStringLiteral("index"), QVariant(index)); + context->insert(QStringLiteral("dup"), QVariant(duplicateCounter)); QString newName = m_template->render(context); diff --git a/src/library/export/trackexportworker.h b/src/library/export/trackexportworker.h index 0bedfe91b121..084d4385041b 100644 --- a/src/library/export/trackexportworker.h +++ b/src/library/export/trackexportworker.h @@ -82,10 +82,10 @@ class TrackExportWorker : public QThread { } // Returns the new filename for the track. Applies the pattern if set. - QString generateFilename(TrackPointer track, int index = 0); + QString generateFilename(TrackPointer track, int index = 0, int dupCounter = 0); /// Applies the filename pattern on track - QString applyPattern(TrackPointer track, int index); + QString applyPattern(TrackPointer track, int index, int duplicateCounter = 0); // Cancels the export after the current copy operation. // May be called from another thread. diff --git a/src/test/trackexport_test.cpp b/src/test/trackexport_test.cpp index 5e940575bcdb..a5c095b54d86 100644 --- a/src/test/trackexport_test.cpp +++ b/src/test/trackexport_test.cpp @@ -326,15 +326,15 @@ TEST_F(TrackExporterTest, PatternExport) { context->insert("t", "t42/"); auto pattern = QStringLiteral( "{{t}}{{track.baseName}}-{{track.extension}}-" - "{{track.bpm}}{% if index %}-{{index}}{%endif%}"); + "{{track.bpm}}{% if index %}-{{index}}{%endif%}#{{ dup }}"); TrackExportWorker worker(m_exportDir.canonicalPath(), tracks, &pattern, context); EXPECT_EQ(worker.generateFilename(track1, 0), - QStringLiteral("t42/cover-test-ogg-0")); + QStringLiteral("t42/cover-test-ogg-0#0")); EXPECT_EQ(worker.generateFilename(track2, 1), - QStringLiteral("t42/cover-test-flac-0-1")); - EXPECT_EQ(worker.generateFilename(track3, 0), - QStringLiteral("t42/cover-test-itunes-12-m4a-0")); + QStringLiteral("t42/cover-test-flac-0-1#0")); + EXPECT_EQ(worker.generateFilename(track3, 0, 23), + QStringLiteral("t42/cover-test-itunes-12-m4a-0#23")); auto pattern2 = QStringLiteral("{{track.fileName}}"); worker.setPattern(&pattern2); From 0d9618e23867a105c5af434c71d122c9754d3493 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Tue, 19 Jan 2021 02:08:03 +0100 Subject: [PATCH 08/36] Add support for exporting files from context menu does not require you to create a playlist for exporting files --- src/library/export/trackexportdlg.cpp | 4 ++- src/widget/wtrackmenu.cpp | 41 +++++++++++++++++++++++++-- src/widget/wtrackmenu.h | 7 ++++- src/widget/wtrackproperty.cpp | 1 + src/widget/wtracktext.cpp | 1 + src/widget/wtrackwidgetgroup.cpp | 1 + 6 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/library/export/trackexportdlg.cpp b/src/library/export/trackexportdlg.cpp index 6721a83bbe02..0c1e90642e12 100644 --- a/src/library/export/trackexportdlg.cpp +++ b/src/library/export/trackexportdlg.cpp @@ -209,7 +209,9 @@ void TrackExportDlg::slotResult(TrackExportWorker::ExportResult result, const QS } void TrackExportDlg::slotProgress(const QString from, const QString to, int progress, int count) { - addStatus(from, to); + if (!from.isEmpty() || !to.isEmpty()) { + addStatus(from, to); + } exportProgress->setMinimum(0); exportProgress->setMaximum(count); exportProgress->setValue(progress); diff --git a/src/widget/wtrackmenu.cpp b/src/widget/wtrackmenu.cpp index d0fee8d902f2..d5ac6b14b0c4 100644 --- a/src/widget/wtrackmenu.cpp +++ b/src/widget/wtrackmenu.cpp @@ -12,6 +12,7 @@ #include "library/dlgtagfetcher.h" #include "library/dlgtrackinfo.h" #include "library/dlgtrackmetadataexport.h" +#include "library/export/trackexportdlg.h" #include "library/externaltrackcollection.h" #include "library/library.h" #include "library/librarytablemodel.h" @@ -165,6 +166,9 @@ void WTrackMenu::createMenus() { m_pLibrary->searchTracksInCollection(searchQuery); }); } + // file menu used for browse/export and other features + m_pFileMenu = new QMenu(this); + m_pFileMenu->setTitle(tr("File")); } void WTrackMenu::createActions() { @@ -214,10 +218,15 @@ void WTrackMenu::createActions() { } if (featureIsEnabled(Feature::FileBrowser)) { - m_pFileBrowserAct = new QAction(tr("Open in File Browser"), this); + m_pFileBrowserAct = new QAction(tr("Open in File Browser"), m_pFileMenu); connect(m_pFileBrowserAct, &QAction::triggered, this, &WTrackMenu::slotOpenInFileBrowser); } + if (featureIsEnabled(Feature::Export)) { + m_pFileExportAct = new QAction(tr("Export Files"), m_pFileMenu); + connect(m_pFileExportAct, &QAction::triggered, this, &WTrackMenu::slotExportFiles); + } + if (featureIsEnabled(Feature::Metadata)) { m_pImportMetadataFromFileAct = new QAction(tr("Import From File Tags"), m_pMetadataMenu); @@ -485,8 +494,17 @@ void WTrackMenu::setupActions() { } } - if (featureIsEnabled(Feature::FileBrowser)) { - addAction(m_pFileBrowserAct); + if (featureIsEnabled(Feature::FileBrowser) || + featureIsEnabled(Feature::Export)) { + addMenu(m_pFileMenu); + + if (featureIsEnabled(Feature::FileBrowser)) { + m_pFileMenu->addAction(m_pFileBrowserAct); + } + + if (featureIsEnabled(Feature::Export)) { + m_pFileMenu->addAction(m_pFileExportAct); + } } if (featureIsEnabled(Feature::Properties)) { @@ -897,6 +915,21 @@ void WTrackMenu::slotOpenInFileBrowser() { mixxx::DesktopHelper::openInFileBrowser(locations); } +void WTrackMenu::slotExportFiles() { + const auto pTrackPointerIter = newTrackPointerIterator(); + if (!pTrackPointerIter) { + // Empty, i.e. nothing to do + return; + } + auto trackList = TrackPointerList(); + while (auto nextTrackPointer = pTrackPointerIter->nextItem()) { + const auto pTrack = *nextTrackPointer; + trackList.append(pTrack); + } + TrackExportDlg* exporter = new TrackExportDlg(nullptr, m_pConfig, trackList); + exporter->open(); +} + namespace { class ImportMetadataFromFileTagsTrackPointerOperation : public mixxx::TrackPointerOperation { @@ -1785,6 +1818,8 @@ bool WTrackMenu::featureIsEnabled(Feature flag) const { m_pTrackModel->hasCapabilities(TrackModel::Capability::Purge); case Feature::FileBrowser: return true; + case Feature::Export: + return true; case Feature::Properties: return m_pTrackModel->hasCapabilities(TrackModel::Capability::EditMetadata); case Feature::SearchRelated: diff --git a/src/widget/wtrackmenu.h b/src/widget/wtrackmenu.h index 71f5c841f0cb..85c774298d9a 100644 --- a/src/widget/wtrackmenu.h +++ b/src/widget/wtrackmenu.h @@ -46,10 +46,11 @@ class WTrackMenu : public QMenu { FileBrowser = 1 << 10, Properties = 1 << 11, SearchRelated = 1 << 12, + Export = 1 << 13, TrackModelFeatures = Remove | HideUnhidePurge, All = AutoDJ | LoadTo | Playlist | Crate | Remove | Metadata | Reset | BPM | Color | HideUnhidePurge | FileBrowser | Properties | - SearchRelated + SearchRelated | Export }; Q_DECLARE_FLAGS(Features, Feature) @@ -81,6 +82,7 @@ class WTrackMenu : public QMenu { private slots: // File void slotOpenInFileBrowser(); + void slotExportFiles(); // Row color void slotColorPicked(const mixxx::RgbColor::optional_t& color); @@ -203,6 +205,7 @@ class WTrackMenu : public QMenu { QMenu* m_pClearMetadataMenu{}; QMenu* m_pBPMMenu{}; QMenu* m_pColorMenu{}; + QMenu* m_pFileMenu{}; WCoverArtMenu* m_pCoverMenu{}; parented_ptr m_pSearchRelatedMenu; @@ -234,6 +237,8 @@ class WTrackMenu : public QMenu { // Open file in default file browser QAction* m_pFileBrowserAct{}; + // Export files + QAction* m_pFileExportAct{}; // BPM feature QAction* m_pBpmLockAction{}; diff --git a/src/widget/wtrackproperty.cpp b/src/widget/wtrackproperty.cpp index 67cea1b497b7..b3a12f8cc160 100644 --- a/src/widget/wtrackproperty.cpp +++ b/src/widget/wtrackproperty.cpp @@ -19,6 +19,7 @@ constexpr WTrackMenu::Features kTrackMenuFeatures = WTrackMenu::Feature::BPM | WTrackMenu::Feature::Color | WTrackMenu::Feature::FileBrowser | + WTrackMenu::Feature::Export | WTrackMenu::Feature::Properties; } // namespace diff --git a/src/widget/wtracktext.cpp b/src/widget/wtracktext.cpp index 3c1ddcf7a4cf..9f4fd77ae426 100644 --- a/src/widget/wtracktext.cpp +++ b/src/widget/wtracktext.cpp @@ -19,6 +19,7 @@ constexpr WTrackMenu::Features kTrackMenuFeatures = WTrackMenu::Feature::BPM | WTrackMenu::Feature::Color | WTrackMenu::Feature::FileBrowser | + WTrackMenu::Feature::Export | WTrackMenu::Feature::Properties; } // namespace diff --git a/src/widget/wtrackwidgetgroup.cpp b/src/widget/wtrackwidgetgroup.cpp index df6fc93dadc4..1c9375b6fa04 100644 --- a/src/widget/wtrackwidgetgroup.cpp +++ b/src/widget/wtrackwidgetgroup.cpp @@ -23,6 +23,7 @@ constexpr WTrackMenu::Features kTrackMenuFeatures = WTrackMenu::Feature::BPM | WTrackMenu::Feature::Color | WTrackMenu::Feature::FileBrowser | + WTrackMenu::Feature::Export | WTrackMenu::Feature::Properties; } // anonymous namespace From d129f2d9abcc3ee4f45d88160847a674828d2514 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Wed, 20 Jan 2021 00:54:27 +0100 Subject: [PATCH 09/36] Export crate summary into file exporter context --- CMakeLists.txt | 1 + src/library/export/trackexportdlg.cpp | 10 +++----- src/library/export/trackexportdlg.h | 6 ++--- src/library/export/trackexportworker.cpp | 28 ++++++++++----------- src/library/export/trackexportworker.h | 3 +-- src/library/trackset/crate/cratefeature.cpp | 7 +++--- src/library/trackset/crate/cratesummary.cpp | 9 +++++++ src/library/trackset/crate/cratesummary.h | 18 ++++++------- src/widget/wtrackmenu.cpp | 1 - 9 files changed, 42 insertions(+), 41 deletions(-) create mode 100644 src/library/trackset/crate/cratesummary.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 16b52f2570ad..2080aa5adacb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -645,6 +645,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/library/trackset/crate/cratefeaturehelper.cpp src/library/trackset/crate/cratestorage.cpp src/library/trackset/crate/cratetablemodel.cpp + src/library/trackset/crate/cratesummary.cpp src/library/trackset/playlistfeature.cpp src/library/trackset/setlogfeature.cpp src/library/trackset/tracksettablemodel.cpp diff --git a/src/library/export/trackexportdlg.cpp b/src/library/export/trackexportdlg.cpp index 0c1e90642e12..1066067e3dc7 100644 --- a/src/library/export/trackexportdlg.cpp +++ b/src/library/export/trackexportdlg.cpp @@ -17,8 +17,7 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, Ui::DlgTrackExport(), m_pConfig(pConfig), m_tracks(tracks), - m_worker(nullptr), - m_context(context) { + m_worker(nullptr) { setupUi(this); QString lastExportDirectory = m_pConfig->getValue( @@ -26,7 +25,7 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, QStandardPaths::writableLocation(QStandardPaths::MusicLocation)); folderEdit->setText(lastExportDirectory); - m_worker = new TrackExportWorker(folderEdit->text(), m_tracks); + m_worker = new TrackExportWorker(folderEdit->text(), m_tracks, context); connect(cancelButton, &QPushButton::clicked, @@ -96,9 +95,6 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, } TrackExportDlg::~TrackExportDlg() { - if (m_context) { - delete m_context; - } } bool TrackExportDlg::browseFolder() { @@ -162,7 +158,7 @@ void TrackExportDlg::updatePreview() { QString pattern = comboPattern->currentText(); m_worker->setPattern(&pattern); m_worker->setDestDir(folderEdit->text()); - previewLabel->setText(m_worker->applyPattern(m_tracks[0], 0)); + previewLabel->setText(m_worker->applyPattern(m_tracks[0], 1)); errorLabel->setText(m_worker->errorMessage()); } diff --git a/src/library/export/trackexportdlg.h b/src/library/export/trackexportdlg.h index 12c1a0535e6e..4e58c19636e8 100644 --- a/src/library/export/trackexportdlg.h +++ b/src/library/export/trackexportdlg.h @@ -24,8 +24,9 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { SKIP_ALL, }; - // The dialog is prepared, but not shown on construction. Does not - // take ownership of the export worker. + /// The dialog is prepared, but not shown on construction. + /// You can pass additional information through a Context. + /// The dialog will take ownership of the Context. TrackExportDlg(QWidget* parent, UserSettingsPointer pConfig, TrackPointerList& tracks, @@ -60,7 +61,6 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { UserSettingsPointer m_pConfig; TrackPointerList m_tracks; TrackExportWorker* m_worker; - Grantlee::Context* m_context; int m_errorCount = 0; int m_skippedCount = 0; int m_okCount = 0; diff --git a/src/library/export/trackexportworker.cpp b/src/library/export/trackexportworker.cpp index 7ee0f1a0e56d..9f4c88ffff6b 100644 --- a/src/library/export/trackexportworker.cpp +++ b/src/library/export/trackexportworker.cpp @@ -36,14 +36,19 @@ QString rewriteFilename(const QFileInfo& fileinfo, int index) { TrackExportWorker::TrackExportWorker(const QString& destDir, TrackPointerList& tracks, - QString* pattern, Grantlee::Context* context) : m_running(false), m_destDir(destDir), m_tracks(tracks), m_context(context) { qRegisterMetaType("TrackExportWorker::ExportResult"); - setPattern(pattern); + if (!m_context) { + m_context = new Grantlee::Context(); + } +} + +TrackExportWorker::~TrackExportWorker() { + delete m_context; } // Iterate over a list of tracks and generate a minimal set of files to copy. @@ -197,23 +202,18 @@ QString TrackExportWorker::applyPattern( qWarning() << "engine missing"; return QString(); } - - Grantlee::Context* context = m_context; - if (context == nullptr) { - context = new Grantlee::Context(); - } // fill the context with the proper variables - context->push(); - context->insert(QStringLiteral("directory"), m_destDir); + m_context->push(); + m_context->insert(QStringLiteral("directory"), m_destDir); // this is safe since the context stack is popped after rendering - context->insert(QStringLiteral("track"), track.get()); - context->insert(QStringLiteral("index"), QVariant(index)); - context->insert(QStringLiteral("dup"), QVariant(duplicateCounter)); + m_context->insert(QStringLiteral("track"), track.get()); + m_context->insert(QStringLiteral("index"), QVariant(index)); + m_context->insert(QStringLiteral("dup"), QVariant(duplicateCounter)); - QString newName = m_template->render(context); + QString newName = m_template->render(m_context); // remove the context stack so it is clean again - context->pop(); + m_context->pop(); // replace bad filename characters with spaces return newName.replace(kBadFileCharacters, QStringLiteral(" ")); diff --git a/src/library/export/trackexportworker.h b/src/library/export/trackexportworker.h index 084d4385041b..433100997b73 100644 --- a/src/library/export/trackexportworker.h +++ b/src/library/export/trackexportworker.h @@ -52,10 +52,9 @@ class TrackExportWorker : public QThread { // pattern will TrackExportWorker(const QString& destDir, TrackPointerList& tracks, - QString* pattern = nullptr, Grantlee::Context* context = nullptr); - virtual ~TrackExportWorker() { }; + virtual ~TrackExportWorker(); // exports ALL the tracks. Thread joins on success or failure. void run() override; diff --git a/src/library/trackset/crate/cratefeature.cpp b/src/library/trackset/crate/cratefeature.cpp index 8e39906ed404..356ffec84cf7 100644 --- a/src/library/trackset/crate/cratefeature.cpp +++ b/src/library/trackset/crate/cratefeature.cpp @@ -743,11 +743,12 @@ void CrateFeature::slotExportTrackFiles() { trackpointers.push_back(m_crateTableModel.getTrack(index)); } + // ownership is transferred to TrackExportDlg Grantlee::Context* context = new Grantlee::Context(); - // auto summary = new CrateSummary(); - // m_pTrackCollection->crates().readCrateSummaryById(id, summary); - // context->insert(QStringLiteral("crate"), summary); + auto summary = new CrateSummary(); + m_pTrackCollection->crates().readCrateSummaryById(id, summary); + context->insert(QStringLiteral("crate"), summary); auto exportDialog = new TrackExportDlg(nullptr, m_pConfig, trackpointers, context); exportDialog->open(); diff --git a/src/library/trackset/crate/cratesummary.cpp b/src/library/trackset/crate/cratesummary.cpp new file mode 100644 index 000000000000..0027d2401014 --- /dev/null +++ b/src/library/trackset/crate/cratesummary.cpp @@ -0,0 +1,9 @@ +#include "library/trackset/crate/cratesummary.h" + +CrateSummary::CrateSummary(CrateId id) + : Crate(id), + m_trackCount(0), + m_trackDuration(0.0) { +} + +CrateSummary::~CrateSummary(){}; diff --git a/src/library/trackset/crate/cratesummary.h b/src/library/trackset/crate/cratesummary.h index 8d6da188eaf8..701f2493cb18 100644 --- a/src/library/trackset/crate/cratesummary.h +++ b/src/library/trackset/crate/cratesummary.h @@ -6,19 +6,15 @@ #include "util/duration.h" // A crate with aggregated track properties (total count + duration) -class CrateSummary : public Crate { - // Q_OBJECT +class CrateSummary : public QObject, public Crate { + Q_OBJECT public: - // Q_PROPERTY(uint trackCount READ getTrackCount WRITE setTrackCount) - // Q_PROPERTY(double trackDuration READ getTrackDuration WRITE setTrackDuration) - // Q_PROPERTY(QString name READ getName WRITE setName) + Q_PROPERTY(uint trackCount READ getTrackCount WRITE setTrackCount) + Q_PROPERTY(double trackDuration READ getTrackDuration WRITE setTrackDuration) + Q_PROPERTY(QString name READ getName WRITE setName) - CrateSummary(CrateId id = CrateId()) - : Crate(id), - m_trackCount(0), - m_trackDuration(0.0) { - } - ~CrateSummary() override{}; + CrateSummary(CrateId id = CrateId()); + ~CrateSummary() override; // The number of all tracks in this crate uint getTrackCount() const { diff --git a/src/widget/wtrackmenu.cpp b/src/widget/wtrackmenu.cpp index d5ac6b14b0c4..c52cccb9cff9 100644 --- a/src/widget/wtrackmenu.cpp +++ b/src/widget/wtrackmenu.cpp @@ -64,7 +64,6 @@ WTrackMenu::WTrackMenu( // Remove unsupported features m_eActiveFeatures &= !m_eTrackModelFeatures; } - createMenus(); createActions(); setupActions(); From e2d9ec9b38acf48bde535137539386ff6b184f95 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Wed, 20 Jan 2021 01:16:05 +0100 Subject: [PATCH 10/36] Add render function which does not escape html characters --- src/library/export/trackexportworker.cpp | 2 +- src/util/formatter.cpp | 32 ++++++++++++++++++++++++ src/util/formatter.h | 6 +++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/library/export/trackexportworker.cpp b/src/library/export/trackexportworker.cpp index 9f4c88ffff6b..57e9cd0f6408 100644 --- a/src/library/export/trackexportworker.cpp +++ b/src/library/export/trackexportworker.cpp @@ -210,7 +210,7 @@ QString TrackExportWorker::applyPattern( m_context->insert(QStringLiteral("index"), QVariant(index)); m_context->insert(QStringLiteral("dup"), QVariant(duplicateCounter)); - QString newName = m_template->render(m_context); + QString newName = Formatter::renderNoEscape(m_template, *m_context); // remove the context stack so it is clean again m_context->pop(); diff --git a/src/util/formatter.cpp b/src/util/formatter.cpp index 721b12c4e9b8..d4d6b66e2f57 100644 --- a/src/util/formatter.cpp +++ b/src/util/formatter.cpp @@ -1,9 +1,41 @@ #include "util/formatter.h" +#include #include +#include +#include + +#include +#include + +class NoEscapeStream : public Grantlee::OutputStream { + public: + NoEscapeStream() + : Grantlee::OutputStream() { + } + NoEscapeStream(QTextStream* stream) + : Grantlee::OutputStream(stream) { + } + ~NoEscapeStream() override{}; + + QString escape(const QString& input) const override { + return input; + } + QSharedPointer clone(QTextStream* stream) const override { + return QSharedPointer::create(stream); + } +}; Grantlee::Engine* Formatter::getEngine(QObject* parent) { auto engine = new Grantlee::Engine(parent); // register custom return engine; } + +QString Formatter::renderNoEscape(Grantlee::Template& tmpl, Grantlee::Context& context) { + auto rendered = QString(); + auto stream = QTextStream(&rendered); + NoEscapeStream os(&stream); + tmpl->render(&os, &context); + return rendered; +} diff --git a/src/util/formatter.h b/src/util/formatter.h index 72938bde5370..34595c30ef89 100644 --- a/src/util/formatter.h +++ b/src/util/formatter.h @@ -1,10 +1,16 @@ #pragma once #include +#include #include +#include class Formatter { public: static Grantlee::Engine* getEngine(QObject* parent); + // render template without escaping html characters + static QString renderNoEscape( + Grantlee::Template& templ, + Grantlee::Context& context); }; From 19f00f6118a8b14195ab6a28f2303316b542c98f Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Thu, 21 Jan 2021 23:00:11 +0100 Subject: [PATCH 11/36] Add grantlee plugin with mixxx specific filters --- CMakeLists.txt | 14 +++ src/test/formatter_test.cpp | 57 +++++++++++++ src/util/formatter.cpp | 1 + src/util/formatterplugin/mixxxformatter.cpp | 95 +++++++++++++++++++++ src/util/formatterplugin/mixxxformatter.h | 37 ++++++++ 5 files changed, 204 insertions(+) create mode 100644 src/test/formatter_test.cpp create mode 100644 src/util/formatterplugin/mixxxformatter.cpp create mode 100644 src/util/formatterplugin/mixxxformatter.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2080aa5adacb..1d295bcf2f30 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1406,6 +1406,7 @@ add_executable(mixxx-test src/test/enginemastertest.cpp src/test/enginemicrophonetest.cpp src/test/enginesynctest.cpp + src/test/formatter_test.cpp src/test/globaltrackcache_test.cpp src/test/hotcuecontrol_test.cpp src/test/imageutils_test.cpp @@ -1811,6 +1812,19 @@ target_link_libraries(mixxx-lib PUBLIC Grantlee5::TextDocument ) +# grantlee plugin +add_library(mixxxformatter MODULE + src/util/formatterplugin/mixxxformatter.cpp +) + +set_target_properties(mixxxformatter PROPERTIES AUTOMOC ON) + +grantlee_adjust_plugin_name(mixxxformatter) + +target_link_libraries(mixxxformatter + Grantlee5::Templates +) + # LAME find_package(LAME REQUIRED) target_link_libraries(mixxx-lib PUBLIC LAME::LAME) diff --git a/src/test/formatter_test.cpp b/src/test/formatter_test.cpp new file mode 100644 index 000000000000..9b01a77d957a --- /dev/null +++ b/src/test/formatter_test.cpp @@ -0,0 +1,57 @@ +#include "util/formatter.h" + +#include +#include + +#include "test/mixxxtest.h" + +class FormatterTest : public testing::Test { +}; + +using namespace Grantlee; + +TEST_F(FormatterTest, TestRangeGroupFilter) { + // Generate a file name for the temporary file + auto engine = Formatter::getEngine(nullptr); + auto context = new Context(); + //context->insert("x1", "122"); + Template t1 = engine->newTemplate(QStringLiteral("{{143|rangegroup}}"), QStringLiteral("t1")); + + auto pattern = t1->render(context); + + EXPECT_EQ(t1->render(context), + QString("140-150")); + + Template t2 = engine->newTemplate(QStringLiteral("{{x1|rangegroup}}"), QStringLiteral("t2")); + context->insert("x1", "122"); + EXPECT_EQ(t2->render(context), QString("120-130")); + context->insert(QStringLiteral("x1"), QVariant(123)); + EXPECT_EQ(t2->render(context), QString("120-130")); + context->insert(QStringLiteral("x1"), QVariant(130)); + EXPECT_EQ(t2->render(context), QString("130-140")); + context->insert(QStringLiteral("x1"), QVariant(QString("131"))); + EXPECT_EQ(t2->render(context), QString("130-140")); + + // FIXME(XXX) why is filter argument QVariant(Invalid) ??? +#if 0 + // use different grouping size + Template t3 = engine->newTemplate(QStringLiteral("{{ x1|rangegroup:\"5\" }}"), QStringLiteral("t3")); + context->insert("x1", "122"); + qDebug() << t2->render(context); + EXPECT_EQ(t2->render(context), QString("120-125")); + context->insert("x1", QVariant(122.3)); + EXPECT_EQ(t2->render(context), QString("120-125")); + context->insert("x1", QVariant(120.000001)); + EXPECT_EQ(t2->render(context), QString("120-125")); + context->insert("x1", QVariant(119.99999)); + EXPECT_EQ(t2->render(context), QString("115-120")); + + // use float group size + Template t4 = engine->newTemplate(QStringLiteral("{{x1|rangegroup:\"2.5\"}}"), QStringLiteral("t4")); + + context->insert("x1", QVariant(119.99999)); + EXPECT_EQ(t2->render(context), QString("117.5-120")); + context->insert("x1", QVariant(120.0)); + EXPECT_EQ(t2->render(context), QString("120-122.5")); +#endif +} diff --git a/src/util/formatter.cpp b/src/util/formatter.cpp index d4d6b66e2f57..64a7f538c251 100644 --- a/src/util/formatter.cpp +++ b/src/util/formatter.cpp @@ -28,6 +28,7 @@ class NoEscapeStream : public Grantlee::OutputStream { Grantlee::Engine* Formatter::getEngine(QObject* parent) { auto engine = new Grantlee::Engine(parent); + engine->addDefaultLibrary(QStringLiteral("mixxxformatter")); // register custom return engine; } diff --git a/src/util/formatterplugin/mixxxformatter.cpp b/src/util/formatterplugin/mixxxformatter.cpp new file mode 100644 index 000000000000..1050d3cdc893 --- /dev/null +++ b/src/util/formatterplugin/mixxxformatter.cpp @@ -0,0 +1,95 @@ +#include "mixxxformatter.h" + +#include + +#include +#include +#include +#include +#include + +#include "moc_mixxxformatter.cpp" + +using namespace Grantlee; + +namespace { +double kDefaultGroupSize = 10.0; +} + +/// Groups numeric values into ranges of values (parameter is group size, default 10) + +QVariant RangeGroup::doFilter(const QVariant& input, + const QVariant& argument, + bool autoescape) const { + bool ok = false; + SafeString safeInput = qvariant_cast(input); + double finput = 0.0; + if (!safeInput.get().isNull()) { + finput = safeInput.get().toDouble(&ok); + if (!ok) { + qWarning() << input << "rangegroup filter input is not a number"; + return input; + } + } else { + finput = input.toDouble(&ok); + if (!ok) { + qWarning() << input << "rangegroup filter input is not a number"; + return input; + } + } + double modulus = kDefaultGroupSize; + // FIXME(XXX) why is the argument always a QVariant(Invalid) ? + //qDebug() << "arg" << argument << getSafeString(argument).get(); + if (!argument.isNull()) { + SafeString safeModulus = qvariant_cast(argument); + double modulusInput = kDefaultGroupSize; + if (!safeModulus.get().isNull()) { + modulusInput = safeModulus.get().toDouble(&ok); + if (!ok) { + qWarning() << argument << "rangegroup filter group size is not a number"; + } + } else { + modulusInput = argument.toDouble(&ok); + if (!ok) { + qWarning() << argument << "rangegroup filter group size is not a number"; + } + } + if (modulus <= 0.0) { + qWarning() << argument << "rangegroup filter group size is not a positive number"; + modulus = kDefaultGroupSize; + } + } + + double rest = std::fmod(finput, modulus); + double groupStart = finput - rest; + double groupEnd = groupStart + modulus; + QString startString; + QString endString; + if (abs(groupStart - (qRound(groupStart))) < 0.01) { + startString = QString::number(static_cast(groupStart)); + } else { + startString = QString("%1").arg(groupStart, 0, 'f', 2); + } + if (abs(groupEnd - (qRound(groupEnd))) < 0.01) { + endString = QString::number(static_cast(groupEnd)); + } else { + endString = QString("%1").arg(groupEnd, 0, 'f', 2); + } + + return QVariant(startString + QStringLiteral("-") + endString); +} + +QHash FormatterPlugin::nodeFactories(const QString& name) { + Q_UNUSED(name); + QHash nodes; + return nodes; +} + +QHash FormatterPlugin::filters(const QString& name) { + Q_UNUSED(name); + QHash filters; + + filters.insert("rangegroup", new RangeGroup()); + + return filters; +} diff --git a/src/util/formatterplugin/mixxxformatter.h b/src/util/formatterplugin/mixxxformatter.h new file mode 100644 index 000000000000..9d7afb11b13a --- /dev/null +++ b/src/util/formatterplugin/mixxxformatter.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +using namespace Grantlee; + +/// Groups numeric values into ranges of values (parameter is group size, default 10) +class RangeGroup : public Filter { + public: + RangeGroup(){}; + ~RangeGroup(){}; + + QVariant doFilter(const QVariant& input, + const QVariant& arg = {}, + bool autoescape = false) const override; + bool isSafe() const override { + return true; + }; // see the Autoescaping section +}; + +class FormatterPlugin : public QObject, public TagLibraryInterface { + Q_OBJECT + Q_INTERFACES(Grantlee::TagLibraryInterface) + Q_PLUGIN_METADATA(IID "org.grantlee.TagLibraryInterface") + public: + FormatterPlugin(QObject* parent = nullptr) + : QObject(parent){}; + + QHash nodeFactories(const QString& name = QString()); + QHash filters(const QString& name = QString()); +}; From 715355c6135588354bf5543a80a7714e8c02a645 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Thu, 21 Jan 2021 23:01:21 +0100 Subject: [PATCH 12/36] Fix Exporter Test --- src/test/trackexport_test.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/trackexport_test.cpp b/src/test/trackexport_test.cpp index a5c095b54d86..6a3ae5b3c13b 100644 --- a/src/test/trackexport_test.cpp +++ b/src/test/trackexport_test.cpp @@ -327,7 +327,8 @@ TEST_F(TrackExporterTest, PatternExport) { auto pattern = QStringLiteral( "{{t}}{{track.baseName}}-{{track.extension}}-" "{{track.bpm}}{% if index %}-{{index}}{%endif%}#{{ dup }}"); - TrackExportWorker worker(m_exportDir.canonicalPath(), tracks, &pattern, context); + TrackExportWorker worker(m_exportDir.canonicalPath(), tracks, context); + worker.setPattern(&pattern); EXPECT_EQ(worker.generateFilename(track1, 0), QStringLiteral("t42/cover-test-ogg-0#0")); From fc287dc052c82d569638df6014fb99d498e7b199 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Fri, 22 Jan 2021 02:15:55 +0100 Subject: [PATCH 13/36] Use CrateSummaryWrapper as QObject wrapper --- src/library/trackset/crate/cratefeature.cpp | 3 +- src/library/trackset/crate/cratesummary.cpp | 9 ++-- src/library/trackset/crate/cratesummary.h | 52 +++++++++++++++++---- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/library/trackset/crate/cratefeature.cpp b/src/library/trackset/crate/cratefeature.cpp index 356ffec84cf7..ae43fbcbfc86 100644 --- a/src/library/trackset/crate/cratefeature.cpp +++ b/src/library/trackset/crate/cratefeature.cpp @@ -748,7 +748,8 @@ void CrateFeature::slotExportTrackFiles() { auto summary = new CrateSummary(); m_pTrackCollection->crates().readCrateSummaryById(id, summary); - context->insert(QStringLiteral("crate"), summary); + auto summaryWrapper = new CrateSummaryWrapper(*summary); + context->insert(QStringLiteral("crate"), summaryWrapper); auto exportDialog = new TrackExportDlg(nullptr, m_pConfig, trackpointers, context); exportDialog->open(); diff --git a/src/library/trackset/crate/cratesummary.cpp b/src/library/trackset/crate/cratesummary.cpp index 0027d2401014..2ac5f4d576a7 100644 --- a/src/library/trackset/crate/cratesummary.cpp +++ b/src/library/trackset/crate/cratesummary.cpp @@ -1,9 +1,8 @@ #include "library/trackset/crate/cratesummary.h" -CrateSummary::CrateSummary(CrateId id) - : Crate(id), - m_trackCount(0), - m_trackDuration(0.0) { +CrateSummaryWrapper::CrateSummaryWrapper(CrateSummary& summary) + : QObject(nullptr), + m_summary(summary) { } -CrateSummary::~CrateSummary(){}; +CrateSummaryWrapper::~CrateSummaryWrapper(){}; diff --git a/src/library/trackset/crate/cratesummary.h b/src/library/trackset/crate/cratesummary.h index 701f2493cb18..d6d1bf8ae3f4 100644 --- a/src/library/trackset/crate/cratesummary.h +++ b/src/library/trackset/crate/cratesummary.h @@ -6,16 +6,15 @@ #include "util/duration.h" // A crate with aggregated track properties (total count + duration) -class CrateSummary : public QObject, public Crate { - Q_OBJECT +class CrateSummary : public Crate { public: - Q_PROPERTY(uint trackCount READ getTrackCount WRITE setTrackCount) - Q_PROPERTY(double trackDuration READ getTrackDuration WRITE setTrackDuration) - Q_PROPERTY(QString name READ getName WRITE setName) - - CrateSummary(CrateId id = CrateId()); - ~CrateSummary() override; + CrateSummary(CrateId id = CrateId()) + : Crate(id), + m_trackCount(0), + m_trackDuration(0.0) { + } + ~CrateSummary(){}; // The number of all tracks in this crate uint getTrackCount() const { return m_trackCount; @@ -40,3 +39,40 @@ class CrateSummary : public QObject, public Crate { uint m_trackCount; double m_trackDuration; }; + +class CrateSummaryWrapper : public QObject { + Q_OBJECT + public: + Q_PROPERTY(uint trackCount READ getTrackCount WRITE setTrackCount) + Q_PROPERTY(double trackDuration READ getTrackDuration WRITE setTrackDuration) + Q_PROPERTY(QString name READ getName WRITE setName) + + CrateSummaryWrapper(CrateSummary& summary); + ~CrateSummaryWrapper(); + + QString getName() const { + return m_summary.getName(); + }; + void setName(QString name) { + m_summary.setName(name); + }; + + // The number of all tracks in this crate + uint getTrackCount() const { + return m_summary.getTrackCount(); + } + void setTrackCount(uint trackCount) { + m_summary.setTrackCount(trackCount); + } + + // The total duration (in seconds) of all tracks in this crate + double getTrackDuration() const { + return m_summary.getTrackDuration(); + } + void setTrackDuration(double trackDuration) { + m_summary.setTrackDuration(trackDuration); + } + + private: + CrateSummary m_summary; +}; From 993a5e78687b9f97a5ce0b052045ce9dc850bc95 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Fri, 22 Jan 2021 02:17:41 +0100 Subject: [PATCH 14/36] Add zeropad filter for adding 0 prefixes --- src/library/export/dlgtrackexport.ui | 15 +++++++++ src/test/formatter_test.cpp | 27 ++++++++++++++-- src/util/formatterplugin/mixxxformatter.cpp | 35 ++++++++++++++++++--- src/util/formatterplugin/mixxxformatter.h | 16 +++++++++- 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/src/library/export/dlgtrackexport.ui b/src/library/export/dlgtrackexport.ui index 0c5afe761a70..afc16ee0be06 100644 --- a/src/library/export/dlgtrackexport.ui +++ b/src/library/export/dlgtrackexport.ui @@ -150,6 +150,11 @@ {{ track.artist }} - {{track.title }}.{{ track.extension }} + + + {{ index|zeropad:"3"}} - {{ track.artist }} - {{track.title }}.{{ track.extension }} + + {{ track.bpm}} - {{ track.artist }} - {{track.title }}.{{ track.extension }} @@ -160,6 +165,16 @@ {{ track.bpm}}/{{ track.artist }} - {{track.title }}.{{ track.extension }} + + + {{ track.bpm|rangegroup }}/{{ track.artist }} - {{track.title }}.{{ track.extension }} + + + + + {{ crate.name}}/{{ index }} - {{ track.artist }} - {{ track.title }}.{{ track.extension }} + + diff --git a/src/test/formatter_test.cpp b/src/test/formatter_test.cpp index 9b01a77d957a..72ac4a7d675b 100644 --- a/src/test/formatter_test.cpp +++ b/src/test/formatter_test.cpp @@ -31,11 +31,10 @@ TEST_F(FormatterTest, TestRangeGroupFilter) { EXPECT_EQ(t2->render(context), QString("130-140")); context->insert(QStringLiteral("x1"), QVariant(QString("131"))); EXPECT_EQ(t2->render(context), QString("130-140")); - - // FIXME(XXX) why is filter argument QVariant(Invalid) ??? #if 0 + // FIXME(XXX) why is filter argument always QVariant(Invalid) ??? // use different grouping size - Template t3 = engine->newTemplate(QStringLiteral("{{ x1|rangegroup:\"5\" }}"), QStringLiteral("t3")); + Template t3 = engine->newTemplate(QStringLiteral("{{x1|rangegroup:\"5\"}}"), QStringLiteral("t3")); context->insert("x1", "122"); qDebug() << t2->render(context); EXPECT_EQ(t2->render(context), QString("120-125")); @@ -55,3 +54,25 @@ TEST_F(FormatterTest, TestRangeGroupFilter) { EXPECT_EQ(t2->render(context), QString("120-122.5")); #endif } + +TEST_F(FormatterTest, TestZeropadFilter) { + // Generate a file name for the temporary file + auto engine = Formatter::getEngine(nullptr); + auto context = new Context(); + //context->insert("x1", "122"); + Template t1 = engine->newTemplate(QStringLiteral("{{1|zeropad}}"), QStringLiteral("t1")); + + EXPECT_EQ(t1->render(context), + QString("01")); + + Template t2 = engine->newTemplate(QStringLiteral("{{1|zeropad:\"3\"}}"), QStringLiteral("t2")); + + EXPECT_EQ(t2->render(context), + QString("001")); + + Template t3 = engine->newTemplate(QStringLiteral("{{x1|zeropad:\"4\"}}"), QStringLiteral("t2")); + context->insert("x1", QVariant(23)); + + EXPECT_EQ(t3->render(context), + QString("0023")); +} diff --git a/src/util/formatterplugin/mixxxformatter.cpp b/src/util/formatterplugin/mixxxformatter.cpp index 1050d3cdc893..ee3d358b0ca3 100644 --- a/src/util/formatterplugin/mixxxformatter.cpp +++ b/src/util/formatterplugin/mixxxformatter.cpp @@ -17,12 +17,13 @@ double kDefaultGroupSize = 10.0; } /// Groups numeric values into ranges of values (parameter is group size, default 10) - QVariant RangeGroup::doFilter(const QVariant& input, const QVariant& argument, bool autoescape) const { + Q_UNUSED(autoescape); + auto safeInput = getSafeString(input); + bool ok = false; - SafeString safeInput = qvariant_cast(input); double finput = 0.0; if (!safeInput.get().isNull()) { finput = safeInput.get().toDouble(&ok); @@ -37,10 +38,11 @@ QVariant RangeGroup::doFilter(const QVariant& input, return input; } } - double modulus = kDefaultGroupSize; + //double modulus = kDefaultGroupSize; // FIXME(XXX) why is the argument always a QVariant(Invalid) ? - //qDebug() << "arg" << argument << getSafeString(argument).get(); - if (!argument.isNull()) { + double modulus = getSafeString(argument).get().toDouble(&ok); + //qDebug() << "modulus" << modulus << ok; + if (ok) { SafeString safeModulus = qvariant_cast(argument); double modulusInput = kDefaultGroupSize; if (!safeModulus.get().isNull()) { @@ -58,6 +60,8 @@ QVariant RangeGroup::doFilter(const QVariant& input, qWarning() << argument << "rangegroup filter group size is not a positive number"; modulus = kDefaultGroupSize; } + } else { + modulus = kDefaultGroupSize; } double rest = std::fmod(finput, modulus); @@ -79,6 +83,26 @@ QVariant RangeGroup::doFilter(const QVariant& input, return QVariant(startString + QStringLiteral("-") + endString); } +QVariant ZeroPad::doFilter(const QVariant& input, + const QVariant& argument, + bool autoescape) const { + Q_UNUSED(autoescape) + auto value = getSafeString(input); + + bool ok; + int iValue = value.get().toInt(&ok); + if (!ok) + return QString(); + + int arg = getSafeString(argument).get().toInt(&ok); + qDebug() << "zeroarg" << arg << ok; + if (!ok) { + arg = 2; + } + + return SafeString(QString("%1").arg(iValue, arg, 10, QChar('0'))); +} + QHash FormatterPlugin::nodeFactories(const QString& name) { Q_UNUSED(name); QHash nodes; @@ -90,6 +114,7 @@ QHash FormatterPlugin::filters(const QString& name) { QHash filters; filters.insert("rangegroup", new RangeGroup()); + filters.insert("zeropad", new ZeroPad()); return filters; } diff --git a/src/util/formatterplugin/mixxxformatter.h b/src/util/formatterplugin/mixxxformatter.h index 9d7afb11b13a..065cf01a4cfb 100644 --- a/src/util/formatterplugin/mixxxformatter.h +++ b/src/util/formatterplugin/mixxxformatter.h @@ -21,7 +21,21 @@ class RangeGroup : public Filter { bool autoescape = false) const override; bool isSafe() const override { return true; - }; // see the Autoescaping section + }; +}; + +/// Pads a integer with 0 +class ZeroPad : public Filter { + public: + ZeroPad(){}; + ~ZeroPad(){}; + + QVariant doFilter(const QVariant& input, + const QVariant& arg = {}, + bool autoescape = false) const override; + bool isSafe() const override { + return true; + }; }; class FormatterPlugin : public QObject, public TagLibraryInterface { From 515d393a281281ceeb26a732ca396c8406584842 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Fri, 22 Jan 2021 23:23:00 +0100 Subject: [PATCH 15/36] Move exportPlaylistItemsIntoFile to Parser --- src/library/libraryfeature.cpp | 49 --------------- src/library/libraryfeature.h | 8 --- src/library/parser.cpp | 63 +++++++++++++++++++- src/library/parser.h | 5 ++ src/library/trackset/baseplaylistfeature.cpp | 2 +- src/library/trackset/crate/cratefeature.cpp | 2 +- 6 files changed, 68 insertions(+), 61 deletions(-) diff --git a/src/library/libraryfeature.cpp b/src/library/libraryfeature.cpp index 20de2aa31891..c3407c990f6e 100644 --- a/src/library/libraryfeature.cpp +++ b/src/library/libraryfeature.cpp @@ -3,8 +3,6 @@ #include #include "library/library.h" -#include "library/parserm3u.h" -#include "library/parserpls.h" #include "moc_libraryfeature.cpp" #include "util/logger.h" @@ -45,50 +43,3 @@ QStringList LibraryFeature::getPlaylistFiles(QFileDialog::FileMode mode) const { } return dialog.selectedFiles(); } - -bool LibraryFeature::exportPlaylistItemsIntoFile( - QString playlistFilePath, - const QList& playlistItemLocations, - bool useRelativePath) { - if (playlistFilePath.endsWith( - QStringLiteral(".pls"), - Qt::CaseInsensitive)) { - return ParserPls::writePLSFile( - playlistFilePath, - playlistItemLocations, - useRelativePath); - } else if (playlistFilePath.endsWith( - QStringLiteral(".m3u8"), - Qt::CaseInsensitive)) { - return ParserM3u::writeM3U8File( - playlistFilePath, - playlistItemLocations, - useRelativePath); - } else { - //default export to M3U if file extension is missing - if (!playlistFilePath.endsWith( - QStringLiteral(".m3u"), - Qt::CaseInsensitive)) { - kLogger.debug() - << "No valid file extension for playlist export specified." - << "Appending .m3u and exporting to M3U."; - playlistFilePath.append(QStringLiteral(".m3u")); - if (QFileInfo::exists(playlistFilePath)) { - auto overwrite = QMessageBox::question( - nullptr, - tr("Overwrite File?"), - tr("A playlist file with the name \"%1\" already exists.\n" - "The default \"m3u\" extension was added because none was specified.\n\n" - "Do you really want to overwrite it?") - .arg(playlistFilePath)); - if (overwrite != QMessageBox::StandardButton::Yes) { - return false; - } - } - } - return ParserM3u::writeM3UFile( - playlistFilePath, - playlistItemLocations, - useRelativePath); - } -} diff --git a/src/library/libraryfeature.h b/src/library/libraryfeature.h index 465b48c23f21..4227d33f0e5c 100644 --- a/src/library/libraryfeature.h +++ b/src/library/libraryfeature.h @@ -124,14 +124,6 @@ class LibraryFeature : public QObject { void enableCoverArtDisplay(bool); void trackSelected(TrackPointer pTrack); - protected: - // TODO: Move common crate/playlist functions into - // a separate base class - static bool exportPlaylistItemsIntoFile( - QString playlistFilePath, - const QList& playlistItemLocations, - bool useRelativePath); - private: QStringList getPlaylistFiles(QFileDialog::FileMode mode) const; }; diff --git a/src/library/parser.cpp b/src/library/parser.cpp index eca875f3682f..fa1f7b8d5063 100644 --- a/src/library/parser.cpp +++ b/src/library/parser.cpp @@ -11,13 +11,25 @@ // // -#include +#include "library/parser.h" + #include #include #include +#include +#include #include +#include -#include "library/parser.h" +#include "library/parserm3u.h" +#include "library/parserpls.h" +#include "util/logger.h" + +namespace { + +const mixxx::Logger kLogger("Parser"); + +} // anonymous namespace Parser::Parser() { } @@ -158,3 +170,50 @@ TrackFile Parser::playlistEntryToTrackFile( return TrackFile(QDir(basePath), filePath); } } + +bool Parser::exportPlaylistItemsIntoFile( + QString playlistFilePath, + const QList& playlistItemLocations, + bool useRelativePath) { + if (playlistFilePath.endsWith( + QStringLiteral(".pls"), + Qt::CaseInsensitive)) { + return ParserPls::writePLSFile( + playlistFilePath, + playlistItemLocations, + useRelativePath); + } else if (playlistFilePath.endsWith( + QStringLiteral(".m3u8"), + Qt::CaseInsensitive)) { + return ParserM3u::writeM3U8File( + playlistFilePath, + playlistItemLocations, + useRelativePath); + } else { + //default export to M3U if file extension is missing + if (!playlistFilePath.endsWith( + QStringLiteral(".m3u"), + Qt::CaseInsensitive)) { + kLogger.debug() + << "No valid file extension for playlist export specified." + << "Appending .m3u and exporting to M3U."; + playlistFilePath.append(QStringLiteral(".m3u")); + if (QFileInfo::exists(playlistFilePath)) { + auto overwrite = QMessageBox::question( + nullptr, + tr("Overwrite File?"), + tr("A playlist file with the name \"%1\" already exists.\n" + "The default \"m3u\" extension was added because none was specified.\n\n" + "Do you really want to overwrite it?") + .arg(playlistFilePath)); + if (overwrite != QMessageBox::StandardButton::Yes) { + return false; + } + } + } + return ParserM3u::writeM3UFile( + playlistFilePath, + playlistItemLocations, + useRelativePath); + } +} diff --git a/src/library/parser.h b/src/library/parser.h index 4a666bdde812..cc4e7850be45 100644 --- a/src/library/parser.h +++ b/src/library/parser.h @@ -43,6 +43,11 @@ class Parser : public QObject { or 0 in order for the trackimporter to function**/ virtual QList parse(const QString&) = 0; + static bool exportPlaylistItemsIntoFile( + QString playlistFilePath, + const QList& playlistItemLocations, + bool useRelativePath); + protected: // Pointer to the parsed Filelocations QList m_sLocations; diff --git a/src/library/trackset/baseplaylistfeature.cpp b/src/library/trackset/baseplaylistfeature.cpp index 16d7775b40b2..7f76820e17a4 100644 --- a/src/library/trackset/baseplaylistfeature.cpp +++ b/src/library/trackset/baseplaylistfeature.cpp @@ -591,7 +591,7 @@ void BasePlaylistFeature::slotExportPlaylist() { QModelIndex index = pPlaylistTableModel->index(i, 0); playlist_items << pPlaylistTableModel->getTrackLocation(index); } - exportPlaylistItemsIntoFile( + Parser::exportPlaylistItemsIntoFile( file_location, playlist_items, useRelativePath); } } diff --git a/src/library/trackset/crate/cratefeature.cpp b/src/library/trackset/crate/cratefeature.cpp index ae43fbcbfc86..2fac9b963071 100644 --- a/src/library/trackset/crate/cratefeature.cpp +++ b/src/library/trackset/crate/cratefeature.cpp @@ -721,7 +721,7 @@ void CrateFeature::slotExportPlaylist() { QModelIndex index = m_crateTableModel.index(i, 0); playlist_items << m_crateTableModel.getTrackLocation(index); } - exportPlaylistItemsIntoFile( + Parser::exportPlaylistItemsIntoFile( file_location, playlist_items, useRelativePath); From 999edbf574e55976da5cd90a07deb7a50afea7e8 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Sat, 23 Jan 2021 01:38:48 +0100 Subject: [PATCH 16/36] Add support for creating a playlist on file export --- src/library/export/dlgtrackexport.ui | 77 ++++++++++++++++----- src/library/export/trackexportdlg.cpp | 17 ++++- src/library/export/trackexportdlg.h | 3 +- src/library/export/trackexportworker.cpp | 46 +++++++++++- src/library/export/trackexportworker.h | 11 +++ src/library/trackset/crate/cratefeature.cpp | 3 +- 6 files changed, 134 insertions(+), 23 deletions(-) diff --git a/src/library/export/dlgtrackexport.ui b/src/library/export/dlgtrackexport.ui index afc16ee0be06..3ef49cf9c900 100644 --- a/src/library/export/dlgtrackexport.ui +++ b/src/library/export/dlgtrackexport.ui @@ -135,6 +135,34 @@ + + + + + + + + + Browse + + + + + + + + + Folder + + + + + + + Preview + + + @@ -165,6 +193,16 @@ {{ track.bpm}}/{{ track.artist }} - {{track.title }}.{{ track.extension }} + + + {{ track.bpm }}/{{track.key.openkey}} - {{ track.artist }} - {{track.title }}.{{ track.extension }} + + + + + {{ track.bpm }}/{{track.key.lancelot}} - {{ track.artist }} - {{track.title }}.{{ track.extension }} + + {{ track.bpm|rangegroup }}/{{ track.artist }} - {{track.title }}.{{ track.extension }} @@ -184,34 +222,39 @@ - - + + - Folder + Create Playlist - - + + - + - - - Browse - + + + + .m3u8 + + + + + .m3u + + + + + .pls + + - - - - Preview - - - diff --git a/src/library/export/trackexportdlg.cpp b/src/library/export/trackexportdlg.cpp index 1066067e3dc7..902fcf317da2 100644 --- a/src/library/export/trackexportdlg.cpp +++ b/src/library/export/trackexportdlg.cpp @@ -12,7 +12,8 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, UserSettingsPointer pConfig, TrackPointerList& tracks, - Grantlee::Context* context) + Grantlee::Context* context, + const QString* playlist) : QDialog(parent), Ui::DlgTrackExport(), m_pConfig(pConfig), @@ -27,6 +28,10 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, m_worker = new TrackExportWorker(folderEdit->text(), m_tracks, context); + if (playlist) { + playlistName->setText(*playlist); + } + connect(cancelButton, &QPushButton::clicked, this, @@ -118,6 +123,9 @@ void TrackExportDlg::setEnableControls(bool enabled) { comboPattern->setEnabled(enabled); folderEdit->setEnabled(enabled); browseButton->setEnabled(enabled); + playlistName->setEnabled(enabled); + playlistExport->setEnabled(enabled); + playlistSuffix->setEnabled(enabled); } void TrackExportDlg::slotStartExport() { @@ -148,6 +156,13 @@ void TrackExportDlg::slotStartExport() { setEnableControls(false); cancelButton->setText(tr("&Cancel")); + // enable playlist export + if (playlistExport->isChecked()) { + m_worker->setPlaylist(playlistName->text() + playlistSuffix->currentText()); + } else { + m_worker->setPlaylist(QString()); + } + m_worker->start(); } diff --git a/src/library/export/trackexportdlg.h b/src/library/export/trackexportdlg.h index 4e58c19636e8..bf8ee689df8b 100644 --- a/src/library/export/trackexportdlg.h +++ b/src/library/export/trackexportdlg.h @@ -30,7 +30,8 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { TrackExportDlg(QWidget* parent, UserSettingsPointer pConfig, TrackPointerList& tracks, - Grantlee::Context* context = nullptr); + Grantlee::Context* context = nullptr, + const QString* playlistName = nullptr); virtual ~TrackExportDlg(); public slots: diff --git a/src/library/export/trackexportworker.cpp b/src/library/export/trackexportworker.cpp index 57e9cd0f6408..798e22cf358e 100644 --- a/src/library/export/trackexportworker.cpp +++ b/src/library/export/trackexportworker.cpp @@ -11,6 +11,7 @@ #include #include +#include "library/parser.h" #include "moc_trackexportworker.cpp" #include "track/track.h" #include "util/formatter.h" @@ -24,6 +25,7 @@ const auto kResultCanceled = QStringLiteral("Export canceled"); const auto kResultEmptyPattern = QStringLiteral("empty pattern result, skipped"); const auto kResultOk = QStringLiteral("ok"); const auto kResultCantCreateDirectory = QStringLiteral("Could not create folder"); +const auto kResultCantCreateFile = QStringLiteral("Could not create file"); QString rewriteFilename(const QFileInfo& fileinfo, int index) { // We don't have total control over the inputs, so definitely @@ -145,9 +147,15 @@ void TrackExportWorker::run() { int i = 0; auto skippedTracks = TrackPointerList(); QMap copy_list = createCopylist(m_tracks, &skippedTracks); + int jobsTotal = copy_list.size(); + + if (!m_playlist.isEmpty()) { + jobsTotal++; + } + for (TrackPointer track : qAsConst(skippedTracks)) { QString fileName = track->fileName(); - emit progress(fileName, nullptr, 0, copy_list.size()); + emit progress(fileName, nullptr, 0, jobsTotal); emit result(TrackExportWorker::ExportResult::SKIPPED, kResultEmptyPattern); } @@ -158,7 +166,7 @@ void TrackExportWorker::run() { // on the bar, which looks really nice. QString fileName = it->fileName(); QString target = it.key(); - emit progress(fileName, target, i, copy_list.size()); + emit progress(fileName, target, i, jobsTotal); copyFile((*it).asFileInfo(), target); if (atomicLoadAcquire(m_bStop)) { emit canceled(); @@ -167,7 +175,39 @@ void TrackExportWorker::run() { } ++i; } - emit progress(QStringLiteral(""), QStringLiteral(""), copy_list.size(), copy_list.size()); + if (!m_playlist.isEmpty()) { + const auto targetDir = QDir(m_destDir); + const QString plsPath = targetDir.filePath(m_playlist); + QFileInfo plsPathFileinfo(plsPath); + + emit progress(QStringLiteral("export playlist"), m_playlist, i, jobsTotal); + + QDir plsDir = plsPathFileinfo.absoluteDir(); + if (!plsDir.mkpath(plsDir.absolutePath())) { + emit result(TrackExportWorker::ExportResult::FAILED, kResultCantCreateDirectory); + } else { + QStringList playlistItemLocations = QStringList(); + for (auto it = copy_list.constBegin(); it != copy_list.constEnd(); ++it) { + // calculate the relative path between the exported file and the playlist + QString relpath = plsPathFileinfo.dir().relativeFilePath( + targetDir.filePath(it.key())); + playlistItemLocations.append(relpath); + } + bool playlistOk = Parser::exportPlaylistItemsIntoFile( + plsPath, + playlistItemLocations, + true); + if (playlistOk) { + emit result(TrackExportWorker::ExportResult::OK, kResultOk); + } else { + emit result(TrackExportWorker::ExportResult::FAILED, + kResultCantCreateDirectory); + } + } + i++; + } + + emit progress(QStringLiteral(""), QStringLiteral(""), i, jobsTotal); emit result(TrackExportWorker::ExportResult::EXPORT_COMPLETE, kResultOk); m_running = false; } diff --git a/src/library/export/trackexportworker.h b/src/library/export/trackexportworker.h index 433100997b73..7178dad706f5 100644 --- a/src/library/export/trackexportworker.h +++ b/src/library/export/trackexportworker.h @@ -80,6 +80,15 @@ class TrackExportWorker : public QThread { return m_pattern; } + /// Sets the filename for the playlist to generate + void setPlaylist(QString playlist) { + m_playlist = playlist; + } + /// returns the playlist filename + QString getPlaylist() { + return m_playlist; + } + // Returns the new filename for the track. Applies the pattern if set. QString generateFilename(TrackPointer track, int index = 0, int dupCounter = 0); @@ -114,6 +123,7 @@ class TrackExportWorker : public QThread { QMap createCopylist( const TrackPointerList& tracks, TrackPointerList* skippedTracks); + void exportPlaylist(); // Emit a signal requesting overwrite mode, and block until we get an // answer. Updates m_overwriteMode appropriately. @@ -130,4 +140,5 @@ class TrackExportWorker : public QThread { Grantlee::Context* m_context; Grantlee::Template m_template; Grantlee::Engine* m_engine{nullptr}; + QString m_playlist; }; diff --git a/src/library/trackset/crate/cratefeature.cpp b/src/library/trackset/crate/cratefeature.cpp index 2fac9b963071..14fdb332df1d 100644 --- a/src/library/trackset/crate/cratefeature.cpp +++ b/src/library/trackset/crate/cratefeature.cpp @@ -751,7 +751,8 @@ void CrateFeature::slotExportTrackFiles() { auto summaryWrapper = new CrateSummaryWrapper(*summary); context->insert(QStringLiteral("crate"), summaryWrapper); - auto exportDialog = new TrackExportDlg(nullptr, m_pConfig, trackpointers, context); + auto exportDialog = new TrackExportDlg( + nullptr, m_pConfig, trackpointers, context, &summary->getName()); exportDialog->open(); } From 7c865791d57b9a1fda870df66d9925fc95ac854b Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Sun, 22 Nov 2020 23:27:00 +0100 Subject: [PATCH 17/36] Add PlaylistSummary for repersenting playlists --- src/library/dao/playlistdao.cpp | 96 ++++++++++++++++++++++ src/library/dao/playlistdao.h | 5 +- src/library/trackset/baseplaylistfeature.h | 5 +- src/library/trackset/playlistfeature.cpp | 91 +------------------- src/library/trackset/playlistfeature.h | 2 +- src/library/trackset/playlistsummary.h | 79 ++++++++++++++++++ 6 files changed, 185 insertions(+), 93 deletions(-) create mode 100644 src/library/trackset/playlistsummary.h diff --git a/src/library/dao/playlistdao.cpp b/src/library/dao/playlistdao.cpp index 2a64dd36dbfb..a7c48d4e69f1 100644 --- a/src/library/dao/playlistdao.cpp +++ b/src/library/dao/playlistdao.cpp @@ -14,6 +14,7 @@ #include "track/track.h" #include "util/compatibility.h" #include "util/db/fwdsqlquery.h" +#include "util/db/dbconnection.h" #include "util/math.h" PlaylistDAO::PlaylistDAO() @@ -23,6 +24,32 @@ PlaylistDAO::PlaylistDAO() void PlaylistDAO::initialize(const QSqlDatabase& database) { DAO::initialize(database); populatePlaylistMembershipCache(); + + // create temporary views + QString queryString = QLatin1String( + "CREATE TEMPORARY VIEW IF NOT EXISTS PlaylistsCountsDurations " + "AS SELECT " + " Playlists.id AS id, " + " Playlists.name AS name, " + " LOWER(Playlists.name) AS sort_name, " + " COUNT(case library.mixxx_deleted when 0 then 1 else null end) " + " AS count, " + " SUM(case library.mixxx_deleted " + " when 0 then library.duration else 0 end) AS durationSeconds " + "FROM Playlists " + "LEFT JOIN PlaylistTracks " + " ON PlaylistTracks.playlist_id = Playlists.id " + "LEFT JOIN library " + " ON PlaylistTracks.track_id = library.id " + " WHERE Playlists.hidden = 0 " + " GROUP BY Playlists.id"); + queryString.append( + mixxx::DbConnection::collateLexicographically( + " ORDER BY sort_name")); + QSqlQuery query(m_database); + if (!query.exec(queryString)) { + LOG_FAILED_QUERY(query); + } } void PlaylistDAO::populatePlaylistMembershipCache() { @@ -1170,6 +1197,75 @@ void PlaylistDAO::getPlaylistsTrackIsIn(TrackId trackId, } } +QList PlaylistDAO::createPlaylistSummaryForTracks(QList tracks) { + QSet allPlaylistIds; + QSet playlistIds; + QMap trackCount; + for (TrackId trackId : qAsConst(tracks)) { + PlaylistDAO::getPlaylistsTrackIsIn(trackId, &playlistIds); + allPlaylistIds += playlistIds; + for (int playlistId : playlistIds) { + trackCount[playlistId] = trackCount.value(playlistId, 0) + 1; + } + } + QList summaries = PlaylistDAO::createPlaylistSummary(&allPlaylistIds); + for (PlaylistSummary summary : qAsConst(summaries)) { + DEBUG_ASSERT(trackCount.contains(summary.id())); + summary.setMatches(trackCount.value(summary.id())); + } + return summaries; +} + +QList PlaylistDAO::createPlaylistSummary(QSet* playlistIds) { + QList playlistLabels; + + // Setup the sidebar playlist model + QSqlTableModel playlistTableModel(this, m_database); + playlistTableModel.setTable("PlaylistsCountsDurations"); + + if (playlistIds) { + QStringList idList; + for (const auto& playlistId : *playlistIds) { + idList.append(QString::number(playlistId)); + } + playlistTableModel.setFilter(QString("Playlists.id in [%1]").arg(idList.join(","))); + } + + playlistTableModel.select(); + while (playlistTableModel.canFetchMore()) { + playlistTableModel.fetchMore(); + } + QSqlRecord record = playlistTableModel.record(); + int nameColumn = record.indexOf("name"); + int idColumn = record.indexOf("id"); + int countColumn = record.indexOf("count"); + int durationColumn = record.indexOf("durationSeconds"); + + for (int row = 0; row < playlistTableModel.rowCount(); ++row) { + int id = + playlistTableModel + .data(playlistTableModel.index(row, idColumn)) + .toInt(); + QString name = + playlistTableModel + .data(playlistTableModel.index(row, nameColumn)) + .toString(); + int count = + playlistTableModel + .data(playlistTableModel.index(row, countColumn)) + .toInt(); + int duration = + playlistTableModel + .data(playlistTableModel.index(row, durationColumn)) + .toInt(); + PlaylistSummary playlist(id, name); + playlist.setCount(count); + playlist.setDuration(duration); + playlistLabels.append(playlist); + } + return playlistLabels; +} + void PlaylistDAO::setAutoDJProcessor(AutoDJProcessor* pAutoDJProcessor) { m_pAutoDJProcessor = pAutoDJProcessor; } diff --git a/src/library/dao/playlistdao.h b/src/library/dao/playlistdao.h index e7fa7354ab56..0fb5c54de075 100644 --- a/src/library/dao/playlistdao.h +++ b/src/library/dao/playlistdao.h @@ -2,10 +2,11 @@ #include #include -#include #include +#include #include "library/dao/dao.h" +#include "library/trackset/playlistsummary.h" #include "track/trackid.h" #include "util/class.h" @@ -117,6 +118,8 @@ class PlaylistDAO : public QObject, public virtual DAO { bool isTrackInPlaylist(TrackId trackId, const int playlistId) const; void getPlaylistsTrackIsIn(TrackId trackId, QSet* playlistSet) const; + QList createPlaylistSummary(QSet* playlistIds = nullptr); + QList createPlaylistSummaryForTracks(QList tracks); void setAutoDJProcessor(AutoDJProcessor* pAutoDJProcessor); diff --git a/src/library/trackset/baseplaylistfeature.h b/src/library/trackset/baseplaylistfeature.h index 60b18cb9a33a..110d450f7f44 100644 --- a/src/library/trackset/baseplaylistfeature.h +++ b/src/library/trackset/baseplaylistfeature.h @@ -12,6 +12,7 @@ #include "library/dao/playlistdao.h" #include "library/trackset/basetracksetfeature.h" +#include "library/trackset/playlistsummary.h" #include "track/track_decl.h" class WLibrary; @@ -76,10 +77,6 @@ class BasePlaylistFeature : public BaseTrackSetFeature { void slotAnalyzePlaylist(); protected: - struct IdAndLabel { - int id; - QString label; - }; virtual void updateChildModel(int selected_id); virtual void clearChildModel(); diff --git a/src/library/trackset/playlistfeature.cpp b/src/library/trackset/playlistfeature.cpp index 6f07ce493e50..bca56c3c77e4 100644 --- a/src/library/trackset/playlistfeature.cpp +++ b/src/library/trackset/playlistfeature.cpp @@ -21,21 +21,6 @@ #include "widget/wlibrarysidebar.h" #include "widget/wlibrarytextbrowser.h" -namespace { - -QString createPlaylistLabel( - const QString& name, - int count, - int duration) { - return QStringLiteral("%1 (%2) %3") - .arg(name, - QString::number(count), - mixxx::Duration::formatTime( - duration, mixxx::Duration::Precision::SECONDS)); -} - -} // anonymous namespace - PlaylistFeature::PlaylistFeature(Library* pLibrary, UserSettingsPointer pConfig) : BasePlaylistFeature(pLibrary, pConfig, @@ -130,74 +115,6 @@ bool PlaylistFeature::dragMoveAcceptChild(const QModelIndex& index, const QUrl& return !locked && formatSupported; } -QList PlaylistFeature::createPlaylistLabels() { - QSqlDatabase database = - m_pLibrary->trackCollections()->internalCollection()->database(); - - QList playlistLabels; - QString queryString = QStringLiteral( - "CREATE TEMPORARY VIEW IF NOT EXISTS PlaylistsCountsDurations " - "AS SELECT " - " Playlists.id AS id, " - " Playlists.name AS name, " - " LOWER(Playlists.name) AS sort_name, " - " COUNT(case library.mixxx_deleted when 0 then 1 else null end) " - " AS count, " - " SUM(case library.mixxx_deleted " - " when 0 then library.duration else 0 end) AS durationSeconds " - "FROM Playlists " - "LEFT JOIN PlaylistTracks " - " ON PlaylistTracks.playlist_id = Playlists.id " - "LEFT JOIN library " - " ON PlaylistTracks.track_id = library.id " - " WHERE Playlists.hidden = 0 " - " GROUP BY Playlists.id"); - queryString.append( - mixxx::DbConnection::collateLexicographically( - " ORDER BY sort_name")); - QSqlQuery query(database); - if (!query.exec(queryString)) { - LOG_FAILED_QUERY(query); - } - - // Setup the sidebar playlist model - QSqlTableModel playlistTableModel(this, database); - playlistTableModel.setTable("PlaylistsCountsDurations"); - playlistTableModel.select(); - while (playlistTableModel.canFetchMore()) { - playlistTableModel.fetchMore(); - } - QSqlRecord record = playlistTableModel.record(); - int nameColumn = record.indexOf("name"); - int idColumn = record.indexOf("id"); - int countColumn = record.indexOf("count"); - int durationColumn = record.indexOf("durationSeconds"); - - for (int row = 0; row < playlistTableModel.rowCount(); ++row) { - int id = - playlistTableModel - .data(playlistTableModel.index(row, idColumn)) - .toInt(); - QString name = - playlistTableModel - .data(playlistTableModel.index(row, nameColumn)) - .toString(); - int count = - playlistTableModel - .data(playlistTableModel.index(row, countColumn)) - .toInt(); - int duration = - playlistTableModel - .data(playlistTableModel.index(row, durationColumn)) - .toInt(); - BasePlaylistFeature::IdAndLabel idAndLabel; - idAndLabel.id = id; - idAndLabel.label = createPlaylistLabel(name, count, duration); - playlistLabels.append(idAndLabel); - } - return playlistLabels; -} - QString PlaylistFeature::fetchPlaylistLabel(int playlistId) { // Setup the sidebar playlist model QSqlDatabase database = @@ -227,7 +144,7 @@ QString PlaylistFeature::fetchPlaylistLabel(int playlistId) { playlistTableModel .data(playlistTableModel.index(0, durationColumn)) .toInt(); - return createPlaylistLabel(name, count, duration); + return PlaylistSummary::createPlaylistLabel(name, count, duration); } return QString(); } @@ -241,10 +158,10 @@ QModelIndex PlaylistFeature::constructChildModel(int selectedId) { int selectedRow = -1; int row = 0; - const QList playlistLabels = createPlaylistLabels(); + const QList playlistLabels = m_playlistDao.createPlaylistSummary(); for (const auto& idAndLabel : playlistLabels) { - int playlistId = idAndLabel.id; - QString playlistLabel = idAndLabel.label; + int playlistId = idAndLabel.id(); + QString playlistLabel = idAndLabel.getLabel(); if (selectedId == playlistId) { // save index for selection diff --git a/src/library/trackset/playlistfeature.h b/src/library/trackset/playlistfeature.h index 2fe64869d8c8..112dd6fee48f 100644 --- a/src/library/trackset/playlistfeature.h +++ b/src/library/trackset/playlistfeature.h @@ -9,6 +9,7 @@ #include #include "library/trackset/baseplaylistfeature.h" +#include "library/trackset/playlistsummary.h" #include "preferences/usersettings.h" class TrackCollection; @@ -44,7 +45,6 @@ class PlaylistFeature : public BasePlaylistFeature { protected: QString fetchPlaylistLabel(int playlistId) override; void decorateChild(TreeItem* pChild, int playlistId) override; - QList createPlaylistLabels(); QModelIndex constructChildModel(int selectedId); private: diff --git a/src/library/trackset/playlistsummary.h b/src/library/trackset/playlistsummary.h new file mode 100644 index 000000000000..9ddfeec1832e --- /dev/null +++ b/src/library/trackset/playlistsummary.h @@ -0,0 +1,79 @@ +#pragma once + +#include "util/duration.h" + +class PlaylistSummary { + public: + explicit PlaylistSummary(int id = -1, QString label = nullptr) + : m_id(id), + m_name(label), + m_count(0), + m_duration(0), + m_matches(0) { + } + ~PlaylistSummary() = default; + + int id() const { + return m_id; + } + + void setCount(int count) { + m_count = count; + } + int count() const { + return m_count; + } + + void setDuration(int duration) { + m_duration = duration; + } + int duration() const { + return m_duration; + } + + QString name() const { + return m_name; + } + void setName(QString name) { + m_name = name; + } + + int matches() const { + return m_matches; + } + void setMatches(int matches) { + m_matches = matches; + } + + QString getLabel() const { + return createPlaylistLabel( + m_name, + m_count, + m_duration); + } + + static QString createPlaylistLabel( + const QString& name, + int count, + int duration) { + if (!count && !duration) { + return QString(name); + } else { + return QStringLiteral("%1 (%2) %3") + .arg(name, + QString::number(count), + mixxx::Duration::formatTime( + duration, mixxx::Duration::Precision::SECONDS)); + } + } + + private: + int m_id; + QString m_name; + int m_count; + int m_duration; + /// m_matches is used when querying track playlists for + int m_matches; +}; + +Q_DECLARE_METATYPE(PlaylistSummary); From f6fc540a06db18651fbb070e3398c57e5cde282e Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Sat, 23 Jan 2021 13:49:31 +0100 Subject: [PATCH 18/36] Add PlaylistSummary wrapper and export to track exporter --- CMakeLists.txt | 1 + src/library/dao/playlistdao.cpp | 49 ++++++++++++++++- src/library/dao/playlistdao.h | 1 + src/library/trackset/baseplaylistfeature.cpp | 28 +++++++--- src/library/trackset/crate/cratefeature.cpp | 10 +++- src/library/trackset/crate/cratesummary.h | 5 ++ src/library/trackset/playlistsummary.cpp | 8 +++ src/library/trackset/playlistsummary.h | 57 ++++++++++++++++++++ 8 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 src/library/trackset/playlistsummary.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d295bcf2f30..26f2bb64c32b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -647,6 +647,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/library/trackset/crate/cratetablemodel.cpp src/library/trackset/crate/cratesummary.cpp src/library/trackset/playlistfeature.cpp + src/library/trackset/playlistsummary.cpp src/library/trackset/setlogfeature.cpp src/library/trackset/tracksettablemodel.cpp src/library/traktor/traktorfeature.cpp diff --git a/src/library/dao/playlistdao.cpp b/src/library/dao/playlistdao.cpp index a7c48d4e69f1..a5c6616f23be 100644 --- a/src/library/dao/playlistdao.cpp +++ b/src/library/dao/playlistdao.cpp @@ -1228,7 +1228,7 @@ QList PlaylistDAO::createPlaylistSummary(QSet* playlistIds for (const auto& playlistId : *playlistIds) { idList.append(QString::number(playlistId)); } - playlistTableModel.setFilter(QString("Playlists.id in [%1]").arg(idList.join(","))); + playlistTableModel.setFilter(QString("id in [%1]").arg(idList.join(","))); } playlistTableModel.select(); @@ -1265,6 +1265,53 @@ QList PlaylistDAO::createPlaylistSummary(QSet* playlistIds } return playlistLabels; } +PlaylistSummary PlaylistDAO::getPlaylistSummary(int playlistId) { + if (playlistId == -1) { + return PlaylistSummary(); + } + QSqlTableModel playlistTableModel(this, m_database); + playlistTableModel.setTable("PlaylistsCountsDurations"); + + playlistTableModel.setFilter(QString("id = %1").arg(playlistId)); + + playlistTableModel.select(); + while (playlistTableModel.canFetchMore()) { + playlistTableModel.fetchMore(); + } + QSqlRecord record = playlistTableModel.record(); + int nameColumn = record.indexOf("name"); + int idColumn = record.indexOf("id"); + int countColumn = record.indexOf("count"); + int durationColumn = record.indexOf("durationSeconds"); + + if (playlistTableModel.rowCount() == 0) { + return PlaylistSummary(); + } + VERIFY_OR_DEBUG_ASSERT(playlistTableModel.rowCount() == 1) { + qWarning() << "playlist id matched to multiple results"; + } + + int id = + playlistTableModel + .data(playlistTableModel.index(0, idColumn)) + .toInt(); + QString name = + playlistTableModel + .data(playlistTableModel.index(0, nameColumn)) + .toString(); + int count = + playlistTableModel + .data(playlistTableModel.index(0, countColumn)) + .toInt(); + int duration = + playlistTableModel + .data(playlistTableModel.index(0, durationColumn)) + .toInt(); + PlaylistSummary summary(id, name); + summary.setCount(count); + summary.setDuration(duration); + return summary; +} void PlaylistDAO::setAutoDJProcessor(AutoDJProcessor* pAutoDJProcessor) { m_pAutoDJProcessor = pAutoDJProcessor; diff --git a/src/library/dao/playlistdao.h b/src/library/dao/playlistdao.h index 0fb5c54de075..1082a48041d4 100644 --- a/src/library/dao/playlistdao.h +++ b/src/library/dao/playlistdao.h @@ -120,6 +120,7 @@ class PlaylistDAO : public QObject, public virtual DAO { void getPlaylistsTrackIsIn(TrackId trackId, QSet* playlistSet) const; QList createPlaylistSummary(QSet* playlistIds = nullptr); QList createPlaylistSummaryForTracks(QList tracks); + PlaylistSummary getPlaylistSummary(int playlistId); void setAutoDJProcessor(AutoDJProcessor* pAutoDJProcessor); diff --git a/src/library/trackset/baseplaylistfeature.cpp b/src/library/trackset/baseplaylistfeature.cpp index 7f76820e17a4..d6cb1fa2dac3 100644 --- a/src/library/trackset/baseplaylistfeature.cpp +++ b/src/library/trackset/baseplaylistfeature.cpp @@ -15,6 +15,7 @@ #include "library/playlisttablemodel.h" #include "library/trackcollection.h" #include "library/trackcollectionmanager.h" +#include "library/trackset/playlistsummary.h" #include "library/treeitem.h" #include "library/treeitemmodel.h" #include "moc_baseplaylistfeature.cpp" @@ -601,8 +602,12 @@ void BasePlaylistFeature::slotExportTrackFiles() { new PlaylistTableModel(this, m_pLibrary->trackCollections(), "mixxx.db.model.playlist_export")); - - pPlaylistTableModel->setTableModel(playlistIdFromIndex(m_lastRightClickedIndex)); + int id = playlistIdFromIndex(m_lastRightClickedIndex); + VERIFY_OR_DEBUG_ASSERT(id != -1) { + qWarning() << "try to export invalid playlist id"; + return; + } + pPlaylistTableModel->setTableModel(id); pPlaylistTableModel->setSort(pPlaylistTableModel->fieldIndex( ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION), Qt::AscendingOrder); @@ -617,13 +622,20 @@ void BasePlaylistFeature::slotExportTrackFiles() { Grantlee::Context* context = new Grantlee::Context(); - // FIXME(poelzi): make CrateSummary a QObject so it can be inserted into the - // context. Why the linker errors when QObject ? - // auto summary = new CrateSummary(); - // m_pTrackCollection->crates().readCrateSummaryById(id, summary); - // context->insert(QStringLiteral("crate"), summary); + PlaylistSummary summary = m_playlistDao.getPlaylistSummary(id); + PlaylistSummaryWrapper* wrapper = nullptr; + if (summary.isValid()) { + wrapper = new PlaylistSummaryWrapper(summary); + context->insert(QStringLiteral("playlist"), wrapper); + } + QString playlistName = summary.name(); + + auto exportDialog = new TrackExportDlg(nullptr, m_pConfig, tracks, context, &playlistName); + + if (wrapper) { + wrapper->setParent(exportDialog); + } - auto exportDialog = new TrackExportDlg(nullptr, m_pConfig, tracks, context); exportDialog->open(); } diff --git a/src/library/trackset/crate/cratefeature.cpp b/src/library/trackset/crate/cratefeature.cpp index 14fdb332df1d..4af94df9847b 100644 --- a/src/library/trackset/crate/cratefeature.cpp +++ b/src/library/trackset/crate/cratefeature.cpp @@ -748,11 +748,17 @@ void CrateFeature::slotExportTrackFiles() { auto summary = new CrateSummary(); m_pTrackCollection->crates().readCrateSummaryById(id, summary); - auto summaryWrapper = new CrateSummaryWrapper(*summary); - context->insert(QStringLiteral("crate"), summaryWrapper); + CrateSummaryWrapper* summaryWrapper = nullptr; + if (summary->isValid()) { + new CrateSummaryWrapper(*summary); + context->insert(QStringLiteral("crate"), summaryWrapper); + } auto exportDialog = new TrackExportDlg( nullptr, m_pConfig, trackpointers, context, &summary->getName()); + if (summaryWrapper) { + summaryWrapper->setParent(exportDialog); + } exportDialog->open(); } diff --git a/src/library/trackset/crate/cratesummary.h b/src/library/trackset/crate/cratesummary.h index d6d1bf8ae3f4..a2d7dd4be1e7 100644 --- a/src/library/trackset/crate/cratesummary.h +++ b/src/library/trackset/crate/cratesummary.h @@ -15,6 +15,11 @@ class CrateSummary : public Crate { } ~CrateSummary(){}; + + bool isValid() { + return getId().isValid(); + } + // The number of all tracks in this crate uint getTrackCount() const { return m_trackCount; diff --git a/src/library/trackset/playlistsummary.cpp b/src/library/trackset/playlistsummary.cpp new file mode 100644 index 000000000000..9df8a27d620e --- /dev/null +++ b/src/library/trackset/playlistsummary.cpp @@ -0,0 +1,8 @@ +#include "library/trackset/playlistsummary.h" + +PlaylistSummaryWrapper::PlaylistSummaryWrapper(PlaylistSummary& summary) + : m_summary(summary) { +} + +PlaylistSummaryWrapper::~PlaylistSummaryWrapper() { +} diff --git a/src/library/trackset/playlistsummary.h b/src/library/trackset/playlistsummary.h index 9ddfeec1832e..115bb11c3b7a 100644 --- a/src/library/trackset/playlistsummary.h +++ b/src/library/trackset/playlistsummary.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "util/duration.h" class PlaylistSummary { @@ -16,6 +18,9 @@ class PlaylistSummary { int id() const { return m_id; } + bool isValid() const { + return m_id != -1; + } void setCount(int count) { m_count = count; @@ -77,3 +82,55 @@ class PlaylistSummary { }; Q_DECLARE_METATYPE(PlaylistSummary); + +class PlaylistSummaryWrapper : public QObject { + Q_OBJECT + public: + Q_PROPERTY(QString name READ getName WRITE setName) + Q_PROPERTY(int id READ getId) + + Q_PROPERTY(uint trackCount READ getTrackCount WRITE setTrackCount) + Q_PROPERTY(int trackDuration READ getTrackDuration WRITE setTrackDuration) + Q_PROPERTY(int matches READ getMatches WRITE setMatches) + + PlaylistSummaryWrapper(PlaylistSummary& summary); + ~PlaylistSummaryWrapper(); + + int getId() const { + return m_summary.id(); + } + + QString getName() const { + return m_summary.name(); + }; + void setName(QString name) { + m_summary.setName(name); + }; + + // The number of all tracks in this crate + uint getTrackCount() const { + return m_summary.count(); + } + void setTrackCount(uint trackCount) { + m_summary.setCount(trackCount); + } + + // The total duration (in seconds) of all tracks in this crate + int getTrackDuration() const { + return m_summary.duration(); + } + void setTrackDuration(int trackDuration) { + m_summary.setDuration(trackDuration); + } + + // The total duration (in seconds) of all tracks in this crate + int16_t getMatches() const { + return m_summary.matches(); + } + void setMatches(int matches) { + m_summary.setMatches(matches); + } + + private: + PlaylistSummary m_summary; +}; From cc9b5fa0238bda9d7956efbd53d869167e3eab30 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Sat, 23 Jan 2021 14:35:51 +0100 Subject: [PATCH 19/36] Add round filter, move default patterns to code --- src/library/export/dlgtrackexport.ui | 45 --------------------- src/library/export/trackexportdlg.cpp | 34 +++++++++++++++- src/library/export/trackexportdlg.h | 1 + src/test/formatter_test.cpp | 30 +++++++++++++- src/util/formatterplugin/mixxxformatter.cpp | 28 +++++++++++-- src/util/formatterplugin/mixxxformatter.h | 14 +++++++ 6 files changed, 102 insertions(+), 50 deletions(-) diff --git a/src/library/export/dlgtrackexport.ui b/src/library/export/dlgtrackexport.ui index 3ef49cf9c900..bbb1204c70af 100644 --- a/src/library/export/dlgtrackexport.ui +++ b/src/library/export/dlgtrackexport.ui @@ -168,51 +168,6 @@ true - - - {{ track.fileName }} - - - - - {{ track.artist }} - {{track.title }}.{{ track.extension }} - - - - - {{ index|zeropad:"3"}} - {{ track.artist }} - {{track.title }}.{{ track.extension }} - - - - - {{ track.bpm}} - {{ track.artist }} - {{track.title }}.{{ track.extension }} - - - - - {{ track.bpm}}/{{ track.artist }} - {{track.title }}.{{ track.extension }} - - - - - {{ track.bpm }}/{{track.key.openkey}} - {{ track.artist }} - {{track.title }}.{{ track.extension }} - - - - - {{ track.bpm }}/{{track.key.lancelot}} - {{ track.artist }} - {{track.title }}.{{ track.extension }} - - - - - {{ track.bpm|rangegroup }}/{{ track.artist }} - {{track.title }}.{{ track.extension }} - - - - - {{ crate.name}}/{{ index }} - {{ track.artist }} - {{ track.title }}.{{ track.extension }} - - diff --git a/src/library/export/trackexportdlg.cpp b/src/library/export/trackexportdlg.cpp index 902fcf317da2..d30b2366211e 100644 --- a/src/library/export/trackexportdlg.cpp +++ b/src/library/export/trackexportdlg.cpp @@ -9,6 +9,30 @@ #include "moc_trackexportdlg.cpp" #include "util/assert.h" +namespace { + +const QStringList kDefaultPatterns = QStringList() + << "{{ track.fileName }}" + << "{{ track.artist }} - {{track.title }}.{{ track.extension }}" + << "{{ index|zeropad:\"3\"}} - {{ track.artist }} - {{track.title " + "}}.{{ track.extension }}" + << "{{ track.bpm|round }} - {{ track.artist }} - {{track.title }}.{{ " + "track.extension }}" + << "{{ track.bpm|round }}/{{ track.artist }} - {{track.title }}.{{ " + "track.extension }}" + << "{{ track.bpm|round }}/{{ track.key.openkey }} - {{ track.artist }} " + "- {{track.title }}.{{ track.extension }}" + << "{{ track.bpm|round }}/{{ track.key.lancelot }} - {{ track.artist " + "}} - {{track.title }}.{{ track.extension }}" + << "{{ track.bpm|rangegroup }}/{{ track.artist }} - {{track.title " + "}}.{{ track.extension }}" + << "{{ crate.name }}/{{ index }} - {{ track.artist }} - {{ track.title " + "}}.{{ track.extension }}" + << "{{ playlist.name }}/{{ index }} - {{ track.artist }} - {{ " + "track.title }}.{{ track.extension }}"; + +} // anonymous namespace + TrackExportDlg::TrackExportDlg(QWidget* parent, UserSettingsPointer pConfig, TrackPointerList& tracks, @@ -32,6 +56,8 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, playlistName->setText(*playlist); } + populateDefaultPatterns(); + connect(cancelButton, &QPushButton::clicked, this, @@ -102,6 +128,12 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, TrackExportDlg::~TrackExportDlg() { } +void TrackExportDlg::populateDefaultPatterns() { + for (auto pattern : kDefaultPatterns) { + comboPattern->addItem(pattern, QVariant(true)); + } +} + bool TrackExportDlg::browseFolder() { QString destDir = QFileDialog::getExistingDirectory( nullptr, tr("Export Track Files To"), folderEdit->text()); @@ -134,7 +166,7 @@ void TrackExportDlg::slotStartExport() { return; } // Do we want to check for target folder to exist ? - // When I file is exported, all parent folders are created automatically. + // When a file is exported, all parent folders are created automatically. // QString destDir = folderEdit->text(); // if (!QFile::exists(destDir)) { diff --git a/src/library/export/trackexportdlg.h b/src/library/export/trackexportdlg.h index bf8ee689df8b..6d73639a063c 100644 --- a/src/library/export/trackexportdlg.h +++ b/src/library/export/trackexportdlg.h @@ -58,6 +58,7 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { void updatePreview(); void setEnableControls(bool enabled); void closeEvent(QCloseEvent* event) override; + void populateDefaultPatterns(); UserSettingsPointer m_pConfig; TrackPointerList m_tracks; diff --git a/src/test/formatter_test.cpp b/src/test/formatter_test.cpp index 72ac4a7d675b..51316479418d 100644 --- a/src/test/formatter_test.cpp +++ b/src/test/formatter_test.cpp @@ -70,9 +70,37 @@ TEST_F(FormatterTest, TestZeropadFilter) { EXPECT_EQ(t2->render(context), QString("001")); - Template t3 = engine->newTemplate(QStringLiteral("{{x1|zeropad:\"4\"}}"), QStringLiteral("t2")); + Template t3 = engine->newTemplate(QStringLiteral("{{x1|zeropad:\"4\"}}"), QStringLiteral("t3")); context->insert("x1", QVariant(23)); EXPECT_EQ(t3->render(context), QString("0023")); } + +TEST_F(FormatterTest, TestRoundFilter) { + // Generate a file name for the temporary file + auto engine = Formatter::getEngine(nullptr); + auto context = new Context(); + //context->insert("x1", "122"); + Template t1 = engine->newTemplate(QStringLiteral("{{x1|round}}"), QStringLiteral("t1")); + context->insert("x1", QVariant(1.49)); + EXPECT_EQ(t1->render(context), + QString("1")); + context->insert("x1", QVariant(1.51)); + EXPECT_EQ(t1->render(context), + QString("2")); + + context->insert("x1", QVariant(156.49567)); + EXPECT_EQ(t1->render(context), + QString("156")); + + Template t2 = engine->newTemplate(QStringLiteral("{{x1|round:\"2\"}}"), QStringLiteral("t3")); + + context->insert("x1", QVariant(1.234567)); + EXPECT_EQ(t2->render(context), + QString("1.23")); + + context->insert("x1", QVariant(1.23567)); + EXPECT_EQ(t2->render(context), + QString("1.24")); +} diff --git a/src/util/formatterplugin/mixxxformatter.cpp b/src/util/formatterplugin/mixxxformatter.cpp index ee3d358b0ca3..923b15fb3ca4 100644 --- a/src/util/formatterplugin/mixxxformatter.cpp +++ b/src/util/formatterplugin/mixxxformatter.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -91,11 +92,10 @@ QVariant ZeroPad::doFilter(const QVariant& input, bool ok; int iValue = value.get().toInt(&ok); - if (!ok) + if (!ok) { return QString(); - + } int arg = getSafeString(argument).get().toInt(&ok); - qDebug() << "zeroarg" << arg << ok; if (!ok) { arg = 2; } @@ -103,6 +103,27 @@ QVariant ZeroPad::doFilter(const QVariant& input, return SafeString(QString("%1").arg(iValue, arg, 10, QChar('0'))); } +QVariant Rounder::doFilter(const QVariant& input, + const QVariant& argument, + bool autoescape) const { + Q_UNUSED(autoescape) + auto value = getSafeString(input); + + bool ok; + double dValue = value.get().toDouble(&ok); + if (!ok) { + return QString(); + } + int arg = getSafeString(argument).get().toInt(&ok); + if (!ok) { + arg = 0; + } + + double rValue = qRound(dValue * qPow(10, arg)) / qPow(10, arg); + + return SafeString(QString("%1").arg(rValue, 0, 'f', arg)); +} + QHash FormatterPlugin::nodeFactories(const QString& name) { Q_UNUSED(name); QHash nodes; @@ -115,6 +136,7 @@ QHash FormatterPlugin::filters(const QString& name) { filters.insert("rangegroup", new RangeGroup()); filters.insert("zeropad", new ZeroPad()); + filters.insert("round", new Rounder()); return filters; } diff --git a/src/util/formatterplugin/mixxxformatter.h b/src/util/formatterplugin/mixxxformatter.h index 065cf01a4cfb..aeff152f8c23 100644 --- a/src/util/formatterplugin/mixxxformatter.h +++ b/src/util/formatterplugin/mixxxformatter.h @@ -38,6 +38,20 @@ class ZeroPad : public Filter { }; }; +/// Rounds a double to n precission (default = 0) +class Rounder : public Filter { + public: + Rounder(){}; + ~Rounder(){}; + + QVariant doFilter(const QVariant& input, + const QVariant& arg = {}, + bool autoescape = false) const override; + bool isSafe() const override { + return true; + }; +}; + class FormatterPlugin : public QObject, public TagLibraryInterface { Q_OBJECT Q_INTERFACES(Grantlee::TagLibraryInterface) From 998667ffbf7b4d31035c1d2bf929b4ec531e7883 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Sun, 24 Jan 2021 15:03:57 +0100 Subject: [PATCH 20/36] Compile fixes --- .github/workflows/build-checks.yml | 1 + src/library/dao/playlistdao.cpp | 2 +- src/util/formatterplugin/mixxxformatter.cpp | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-checks.yml b/.github/workflows/build-checks.yml index 297851d952f7..e65be40928dc 100644 --- a/.github/workflows/build-checks.yml +++ b/.github/workflows/build-checks.yml @@ -24,6 +24,7 @@ jobs: libebur128-dev \ libfftw3-dev \ libflac-dev \ + libgrantlee5-dev \ libid3tag0-dev \ liblilv-dev \ libmad0-dev \ diff --git a/src/library/dao/playlistdao.cpp b/src/library/dao/playlistdao.cpp index a5c6616f23be..613520dda231 100644 --- a/src/library/dao/playlistdao.cpp +++ b/src/library/dao/playlistdao.cpp @@ -13,8 +13,8 @@ #include "library/trackcollection.h" #include "track/track.h" #include "util/compatibility.h" -#include "util/db/fwdsqlquery.h" #include "util/db/dbconnection.h" +#include "util/db/fwdsqlquery.h" #include "util/math.h" PlaylistDAO::PlaylistDAO() diff --git a/src/util/formatterplugin/mixxxformatter.cpp b/src/util/formatterplugin/mixxxformatter.cpp index 923b15fb3ca4..e75267f1c1d6 100644 --- a/src/util/formatterplugin/mixxxformatter.cpp +++ b/src/util/formatterplugin/mixxxformatter.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include From 1d64ce48444448461c1e6b6bbb0c1c153b902b73 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Tue, 2 Feb 2021 00:51:50 +0100 Subject: [PATCH 21/36] Add function to ensure safe filename on all platforms --- CMakeLists.txt | 1 + src/test/file_test.cpp | 23 +++++++++++++++++++++++ src/util/file.cpp | 14 ++++++++++++++ src/util/file.h | 11 +++++++++++ 4 files changed, 49 insertions(+) create mode 100644 src/test/file_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d9f26fb3419c..97be378552c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1416,6 +1416,7 @@ add_executable(mixxx-test src/test/enginemicrophonetest.cpp src/test/enginesynctest.cpp src/test/formatter_test.cpp + src/test/file_test.cpp src/test/globaltrackcache_test.cpp src/test/hotcuecontrol_test.cpp src/test/imageutils_test.cpp diff --git a/src/test/file_test.cpp b/src/test/file_test.cpp new file mode 100644 index 000000000000..8f5992790c38 --- /dev/null +++ b/src/test/file_test.cpp @@ -0,0 +1,23 @@ +#include "util/file.h" + +#include + +#include "test/mixxxtest.h" + +class FileTest : public testing::Test { +}; + +TEST_F(FileTest, TestSafeFilename) { + // Generate a file name for the temporary file + const auto fileName = QStringLiteral("broken/ <> :\"\\|ok?*!.mp3"); + const auto expected = QStringLiteral("broken# ## ####ok##!.mp3"); + auto output = FileUtils::safeFilename(fileName); + //qDebug() << "replaced" << output; + ASSERT_EQ(expected, output); + + // test 0 byte characters + const auto fileName0 = QStringLiteral("t2\0\10Z"); + auto output2 = FileUtils::safeFilename(fileName0); + //qDebug() << "replaced" << output2; + ASSERT_EQ(QStringLiteral("t2##Z"), output2); +} diff --git a/src/util/file.cpp b/src/util/file.cpp index 962055c2ccfc..6567ad289d68 100644 --- a/src/util/file.cpp +++ b/src/util/file.cpp @@ -1,5 +1,13 @@ #include "util/file.h" +#include + +namespace { +//const auto kIllegalCharacters = QRegExp("[\0/<>:\"\\|\\?\\*]"); +// see https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names +const auto kIllegalCharacters = QRegExp("([/<>:\"\\\\\\|\\?\\*]|[\x01-\x1F])"); +} // namespace + MDir::MDir() { } @@ -28,3 +36,9 @@ MDir& MDir::operator=(const MDir& other) { bool MDir::canAccess() { return Sandbox::canAccessFile(m_dir); } + +QString FileUtils::safeFilename(const QString& input, const QString& replacement) { + auto output = QString(input); + output.replace(kIllegalCharacters, replacement); + return output.replace(QChar::Null, replacement); +} diff --git a/src/util/file.h b/src/util/file.h index d1950d0ca95d..739ee30cb1f9 100644 --- a/src/util/file.h +++ b/src/util/file.h @@ -33,3 +33,14 @@ class MDir { QDir m_dir; SecurityTokenPointer m_pSecurityToken; }; + +namespace { +const QString kDefaultReplacementCharacter = QString("#"); +} + +class FileUtils { + public: + // returns a filename that is safe on all platforms and does not contain unwanted characters like newline + static QString safeFilename(const QString& input, + const QString& replacement = kDefaultReplacementCharacter); +}; From 3057941da98529205a7f61b48d65e224cc3e0ec2 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Tue, 2 Feb 2021 00:56:11 +0100 Subject: [PATCH 22/36] Use tabwidget for export dialog --- src/library/export/dlgtrackexport.ui | 458 +++++++++++++---------- src/library/export/trackexportdlg.cpp | 55 ++- src/library/export/trackexportdlg.h | 1 + src/library/export/trackexportworker.cpp | 25 +- src/library/export/trackexportworker.h | 2 + 5 files changed, 320 insertions(+), 221 deletions(-) diff --git a/src/library/export/dlgtrackexport.ui b/src/library/export/dlgtrackexport.ui index bbb1204c70af..e2cc9862776d 100644 --- a/src/library/export/dlgtrackexport.ui +++ b/src/library/export/dlgtrackexport.ui @@ -6,8 +6,8 @@ 0 0 - 1425 - 958 + 556 + 429 @@ -35,218 +35,292 @@ false - - - - Progress + + + + 1 - - - - - 24 - - - - - - - QAbstractItemView::NoEditTriggers - - - false - - - QAbstractItemView::ExtendedSelection - - - false - - - - From - - - - - Destination + + + &Options + + + + + + true - - - - Result + + + 0 + 0 + - - - - - - - - - - true - - - - 0 - 0 - - - - - - - - - 0 - + - - - - 0 - 0 - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - + + + + + 0 + + + + + + 0 + 0 + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + Browse + + + + + + + + + Folder + + + + + + + Preview + + + + + + + true + + + + + + + Pattern + + + + + + + Create Playlist + + + + + + + + + + + + + .m3u8 + + + + + .m3u + + + + + .pls + + + + + + + - - - - - 0 - 0 - + + + + Qt::Vertical - - + + QSizePolicy::Fixed - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 20 + 20 + - - - - - - - - + - - - Browse - - - - - - - - - Folder - - - - - - - Preview - - - - - - - true - - - - - - - Pattern - - - - - - - Create Playlist - - - - - - - - - - + + + + + + 0 + 0 + + + + Help + + + - - .m3u8 - + + + Qt::Horizontal + + + + 40 + 20 + + + - - .m3u - + + + + 0 + 0 + + + + &Close + + - - .pls - + + + + 0 + 0 + + + + &Start + + - + + + + + + Qt::Vertical + + + + 20 + 40 + + + - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - + + + + + + + &Progress + + + + + + + + 24 + + + + + + + &Cancel + + + + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + QAbstractItemView::ExtendedSelection + + + false + + + + From + + + - &Start + Destination - - - - + + - &Close + Result - - - - - + + + + + diff --git a/src/library/export/trackexportdlg.cpp b/src/library/export/trackexportdlg.cpp index d30b2366211e..4a74684cd1ff 100644 --- a/src/library/export/trackexportdlg.cpp +++ b/src/library/export/trackexportdlg.cpp @@ -62,6 +62,10 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, &QPushButton::clicked, this, &TrackExportDlg::cancelButtonClicked); + connect(progressCancelButton, + &QPushButton::clicked, + this, + &TrackExportDlg::cancelButtonClicked); connect(startButton, &QPushButton::clicked, this, @@ -112,17 +116,9 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, this, &TrackExportDlg::stopWorker); - if (m_tracks.isEmpty()) { - QMessageBox::warning( - nullptr, - tr("Export Error"), - tr("No files selected"), - QMessageBox::Ok, - QMessageBox::Ok); - hide(); - accept(); - } updatePreview(); + setEnableControls(true); + tabWidget->setCurrentIndex(0); } TrackExportDlg::~TrackExportDlg() { @@ -165,16 +161,7 @@ void TrackExportDlg::slotStartExport() { qWarning() << "Export already running"; return; } - // Do we want to check for target folder to exist ? - // When a file is exported, all parent folders are created automatically. - - // QString destDir = folderEdit->text(); - // if (!QFile::exists(destDir)) { - // if (!browseFolder()) { - // return; - // } - // destDir = folderEdit->text(); - // } + m_errorCount = 0; m_okCount = 0; m_skippedCount = 0; @@ -186,7 +173,10 @@ void TrackExportDlg::slotStartExport() { // sets destDirectory and Pattern updatePreview(); setEnableControls(false); + tabWidget->setCurrentIndex(1); + cancelButton->setText(tr("&Cancel")); + progressCancelButton->setText(tr("&Cancel")); // enable playlist export if (playlistExport->isChecked()) { @@ -309,6 +299,31 @@ void TrackExportDlg::stopWorker() { m_worker->wait(); setEnableControls(true); cancelButton->setText(tr("&Close")); + progressCancelButton->setText(tr("&Close")); +} + +void TrackExportDlg::open() { + bool empty = true; + // check if at least one trackpointer is valid + for (TrackPointer track : qAsConst(m_tracks)) { + if (track) { + empty = false; + break; + } + } + + if (empty) { + QMessageBox::warning( + nullptr, + tr("Export Error"), + tr("No files selected"), + QMessageBox::Ok, + QMessageBox::Ok); + hide(); + accept(); + return; + } + QDialog::open(); } void TrackExportDlg::finish() { diff --git a/src/library/export/trackexportdlg.h b/src/library/export/trackexportdlg.h index 6d73639a063c..6ce461c18881 100644 --- a/src/library/export/trackexportdlg.h +++ b/src/library/export/trackexportdlg.h @@ -33,6 +33,7 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { Grantlee::Context* context = nullptr, const QString* playlistName = nullptr); virtual ~TrackExportDlg(); + void open() override; public slots: void slotProgress(const QString from, const QString to, int progress, int count); diff --git a/src/library/export/trackexportworker.cpp b/src/library/export/trackexportworker.cpp index 798e22cf358e..2cc0437e5e2f 100644 --- a/src/library/export/trackexportworker.cpp +++ b/src/library/export/trackexportworker.cpp @@ -14,6 +14,7 @@ #include "library/parser.h" #include "moc_trackexportworker.cpp" #include "track/track.h" +#include "util/file.h" #include "util/formatter.h" namespace { @@ -26,6 +27,9 @@ const auto kResultEmptyPattern = QStringLiteral("empty pattern result, skipped") const auto kResultOk = QStringLiteral("ok"); const auto kResultCantCreateDirectory = QStringLiteral("Could not create folder"); const auto kResultCantCreateFile = QStringLiteral("Could not create file"); +const auto kDefaultPattern = QStringLiteral( + "{{ track.basename }}{% if dup %}-{{dup}}{% endif %}" + ".{{track.extension}}"); QString rewriteFilename(const QFileInfo& fileinfo, int index) { // We don't have total control over the inputs, so definitely @@ -133,7 +137,12 @@ void TrackExportWorker::setPattern(QString* pattern) { m_engine->setSmartTrimEnabled(true); } m_pattern = pattern; - m_template = m_engine->newTemplate(*m_pattern, QStringLiteral("export")); + updateTemplate(); +} +void TrackExportWorker::updateTemplate() { + QString tmpl = m_destDir + QDir::separator().toLatin1() + + (m_pattern ? *m_pattern : kDefaultPattern); + m_template = m_engine->newTemplate(tmpl, QStringLiteral("export")); if (m_template->error()) { m_errorMessage = m_template->errorString(); } else { @@ -214,15 +223,13 @@ void TrackExportWorker::run() { // Returns the new filename for the track. Applies the pattern if set. QString TrackExportWorker::generateFilename(TrackPointer track, int index, int dupCounter) { - if (m_pattern) { - return applyPattern(track, index, dupCounter); - } + return FileUtils::safeFilename(applyPattern(track, index, dupCounter).trimmed()); - const auto trackFile = track->getFileInfo(); - if (dupCounter == 0) { - return trackFile.fileName(); - } - return rewriteFilename(trackFile.asFileInfo(), dupCounter); + // const auto trackFile = track->getFileInfo(); + // if (dupCounter == 0) { + // return QDir(m_destDir).filePath(trackFile.fileName()); + // } + // return rewriteFilename(trackFile.asFileInfo(), dupCounter); } // Applies the pattern on track diff --git a/src/library/export/trackexportworker.h b/src/library/export/trackexportworker.h index 7178dad706f5..a2d87d819807 100644 --- a/src/library/export/trackexportworker.h +++ b/src/library/export/trackexportworker.h @@ -70,6 +70,7 @@ class TrackExportWorker : public QThread { void setDestDir(const QString& destDir) { m_destDir = destDir; + updateTemplate(); } /// Sets the filename pattern @@ -124,6 +125,7 @@ class TrackExportWorker : public QThread { const TrackPointerList& tracks, TrackPointerList* skippedTracks); void exportPlaylist(); + void updateTemplate(); // Emit a signal requesting overwrite mode, and block until we get an // answer. Updates m_overwriteMode appropriately. From dd3d9e605f8904f2a4dab3d70c1d60e507368b0d Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Tue, 2 Feb 2021 00:57:00 +0100 Subject: [PATCH 23/36] cleanup group filter --- src/util/formatterplugin/mixxxformatter.cpp | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/util/formatterplugin/mixxxformatter.cpp b/src/util/formatterplugin/mixxxformatter.cpp index e75267f1c1d6..53587a4b404d 100644 --- a/src/util/formatterplugin/mixxxformatter.cpp +++ b/src/util/formatterplugin/mixxxformatter.cpp @@ -40,24 +40,10 @@ QVariant RangeGroup::doFilter(const QVariant& input, return input; } } - //double modulus = kDefaultGroupSize; - // FIXME(XXX) why is the argument always a QVariant(Invalid) ? + double modulus = getSafeString(argument).get().toDouble(&ok); - //qDebug() << "modulus" << modulus << ok; + if (ok) { - SafeString safeModulus = qvariant_cast(argument); - double modulusInput = kDefaultGroupSize; - if (!safeModulus.get().isNull()) { - modulusInput = safeModulus.get().toDouble(&ok); - if (!ok) { - qWarning() << argument << "rangegroup filter group size is not a number"; - } - } else { - modulusInput = argument.toDouble(&ok); - if (!ok) { - qWarning() << argument << "rangegroup filter group size is not a number"; - } - } if (modulus <= 0.0) { qWarning() << argument << "rangegroup filter group size is not a positive number"; modulus = kDefaultGroupSize; From 3a79ffaaf4d438512c223078740ed191c4b73ea7 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Tue, 2 Feb 2021 11:25:39 +0100 Subject: [PATCH 24/36] Fix missing assignment of crateWrapper --- src/library/export/trackexportworker.cpp | 14 -------------- src/library/trackset/baseplaylistfeature.cpp | 3 ++- src/library/trackset/crate/cratefeature.cpp | 4 +++- src/library/trackset/crate/cratestorage.cpp | 2 ++ 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/library/export/trackexportworker.cpp b/src/library/export/trackexportworker.cpp index 2cc0437e5e2f..454a8e288e5c 100644 --- a/src/library/export/trackexportworker.cpp +++ b/src/library/export/trackexportworker.cpp @@ -30,14 +30,6 @@ const auto kResultCantCreateFile = QStringLiteral("Could not create file"); const auto kDefaultPattern = QStringLiteral( "{{ track.basename }}{% if dup %}-{{dup}}{% endif %}" ".{{track.extension}}"); - -QString rewriteFilename(const QFileInfo& fileinfo, int index) { - // We don't have total control over the inputs, so definitely - // don't use .arg().arg().arg(). - const QString index_str = QString("%1").arg(index, 4, 10, QChar('0')); - return QString("%1-%2.%3").arg(fileinfo.baseName(), index_str, fileinfo.completeSuffix()); -} - } // namespace TrackExportWorker::TrackExportWorker(const QString& destDir, @@ -224,12 +216,6 @@ void TrackExportWorker::run() { // Returns the new filename for the track. Applies the pattern if set. QString TrackExportWorker::generateFilename(TrackPointer track, int index, int dupCounter) { return FileUtils::safeFilename(applyPattern(track, index, dupCounter).trimmed()); - - // const auto trackFile = track->getFileInfo(); - // if (dupCounter == 0) { - // return QDir(m_destDir).filePath(trackFile.fileName()); - // } - // return rewriteFilename(trackFile.asFileInfo(), dupCounter); } // Applies the pattern on track diff --git a/src/library/trackset/baseplaylistfeature.cpp b/src/library/trackset/baseplaylistfeature.cpp index d6cb1fa2dac3..578326a47385 100644 --- a/src/library/trackset/baseplaylistfeature.cpp +++ b/src/library/trackset/baseplaylistfeature.cpp @@ -624,11 +624,12 @@ void BasePlaylistFeature::slotExportTrackFiles() { PlaylistSummary summary = m_playlistDao.getPlaylistSummary(id); PlaylistSummaryWrapper* wrapper = nullptr; + QString playlistName = QString(); if (summary.isValid()) { wrapper = new PlaylistSummaryWrapper(summary); context->insert(QStringLiteral("playlist"), wrapper); + playlistName = summary.name(); } - QString playlistName = summary.name(); auto exportDialog = new TrackExportDlg(nullptr, m_pConfig, tracks, context, &playlistName); diff --git a/src/library/trackset/crate/cratefeature.cpp b/src/library/trackset/crate/cratefeature.cpp index 6ebd9367183d..a2fd249cd22a 100644 --- a/src/library/trackset/crate/cratefeature.cpp +++ b/src/library/trackset/crate/cratefeature.cpp @@ -774,8 +774,10 @@ void CrateFeature::slotExportTrackFiles() { m_pTrackCollection->crates().readCrateSummaryById(id, summary); CrateSummaryWrapper* summaryWrapper = nullptr; if (summary->isValid()) { - new CrateSummaryWrapper(*summary); + summaryWrapper = new CrateSummaryWrapper(*summary); context->insert(QStringLiteral("crate"), summaryWrapper); + } else { + qWarning() << "CrateSummary is empty"; } auto exportDialog = new TrackExportDlg( diff --git a/src/library/trackset/crate/cratestorage.cpp b/src/library/trackset/crate/cratestorage.cpp index d1c5af35c06d..c2484c984ec4 100644 --- a/src/library/trackset/crate/cratestorage.cpp +++ b/src/library/trackset/crate/cratestorage.cpp @@ -390,6 +390,8 @@ bool CrateStorage::readCrateSummaryById( } else { kLogger.warning() << "Crate summary not found by id:" << id; } + } else { + kLogger.warning() << "Error fetching summary" << query.lastError(); } return false; } From 7fad1724f7f7f3c242ac937ffa0ad9db13bddde1 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Wed, 3 Feb 2021 00:02:31 +0100 Subject: [PATCH 25/36] Properly escape problematic filenames on all platforms Use a Custom Formatter to escape all tags that are not universal compatible. --- CMakeLists.txt | 5 +- src/library/export/trackexportdlg.cpp | 20 ++++---- src/library/export/trackexportworker.cpp | 6 +-- src/test/file_test.cpp | 23 --------- src/test/fileutils_test.cpp | 37 ++++++++++++++ src/test/formatter_test.cpp | 12 +++++ src/util/file.cpp | 13 ----- src/util/file.h | 11 ----- src/util/fileutils.cpp | 30 +++++++++++ src/util/fileutils.h | 18 +++++++ src/util/formatter.cpp | 55 ++++++++++++++------- src/util/formatter.h | 26 ++++++++++ src/util/formatterplugin/mixxxformatter.cpp | 11 +++++ src/util/formatterplugin/mixxxformatter.h | 14 ++++++ 14 files changed, 203 insertions(+), 78 deletions(-) delete mode 100644 src/test/file_test.cpp create mode 100644 src/test/fileutils_test.cpp create mode 100644 src/util/fileutils.cpp create mode 100644 src/util/fileutils.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 97be378552c5..3c962f47f07e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -809,6 +809,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/util/duration.cpp src/util/experiment.cpp src/util/file.cpp + src/util/fileutils.cpp src/util/formatter.cpp src/util/imageutils.cpp src/util/indexrange.cpp @@ -1416,7 +1417,7 @@ add_executable(mixxx-test src/test/enginemicrophonetest.cpp src/test/enginesynctest.cpp src/test/formatter_test.cpp - src/test/file_test.cpp + src/test/fileutils_test.cpp src/test/globaltrackcache_test.cpp src/test/hotcuecontrol_test.cpp src/test/imageutils_test.cpp @@ -1813,9 +1814,11 @@ target_link_libraries(mixxx-lib PUBLIC # grantlee plugin add_library(mixxxformatter MODULE src/util/formatterplugin/mixxxformatter.cpp + src/util/fileutils.cpp ) set_target_properties(mixxxformatter PROPERTIES AUTOMOC ON) +target_include_directories(mixxxformatter PRIVATE src) grantlee_adjust_plugin_name(mixxxformatter) diff --git a/src/library/export/trackexportdlg.cpp b/src/library/export/trackexportdlg.cpp index 4a74684cd1ff..52ecf795c842 100644 --- a/src/library/export/trackexportdlg.cpp +++ b/src/library/export/trackexportdlg.cpp @@ -14,22 +14,22 @@ namespace { const QStringList kDefaultPatterns = QStringList() << "{{ track.fileName }}" << "{{ track.artist }} - {{track.title }}.{{ track.extension }}" - << "{{ index|zeropad:\"3\"}} - {{ track.artist }} - {{track.title " - "}}.{{ track.extension }}" - << "{{ track.bpm|round }} - {{ track.artist }} - {{track.title }}.{{ " - "track.extension }}" - << "{{ track.bpm|round }}/{{ track.artist }} - {{track.title }}.{{ " - "track.extension }}" + << "{{ index|zeropad:\"3\"}} - {{ track.artist }} - " + "{{ track.title }}.{{ track.extension }}" + << "{{ track.bpm|round }} - {{ track.artist }} - {{track.title }}" + ".{{ track.extension }}" + << "{{ track.bpm|round }}/{{ track.artist }} - {{track.title }}" + ".{{ track.extension }}" << "{{ track.bpm|round }}/{{ track.key.openkey }} - {{ track.artist }} " "- {{track.title }}.{{ track.extension }}" << "{{ track.bpm|round }}/{{ track.key.lancelot }} - {{ track.artist " "}} - {{track.title }}.{{ track.extension }}" << "{{ track.bpm|rangegroup }}/{{ track.artist }} - {{track.title " "}}.{{ track.extension }}" - << "{{ crate.name }}/{{ index }} - {{ track.artist }} - {{ track.title " - "}}.{{ track.extension }}" - << "{{ playlist.name }}/{{ index }} - {{ track.artist }} - {{ " - "track.title }}.{{ track.extension }}"; + << "{{ crate.name }}/{{ index|zeropad:\"3\" }} - {{ track.artist }}" + " - {{ track.title }}.{{ track.extension }}" + << "{{ playlist.name }}/{{ index }} - {{ track.artist }} - " + "{{ track.title }}.{{ track.extension }}"; } // anonymous namespace diff --git a/src/library/export/trackexportworker.cpp b/src/library/export/trackexportworker.cpp index 454a8e288e5c..8531080c42b1 100644 --- a/src/library/export/trackexportworker.cpp +++ b/src/library/export/trackexportworker.cpp @@ -14,7 +14,7 @@ #include "library/parser.h" #include "moc_trackexportworker.cpp" #include "track/track.h" -#include "util/file.h" +#include "util/fileutils.h" #include "util/formatter.h" namespace { @@ -243,13 +243,13 @@ QString TrackExportWorker::applyPattern( m_context->insert(QStringLiteral("index"), QVariant(index)); m_context->insert(QStringLiteral("dup"), QVariant(duplicateCounter)); - QString newName = Formatter::renderNoEscape(m_template, *m_context); + QString newName = Formatter::renderFilenameEscape(m_template, *m_context); // remove the context stack so it is clean again m_context->pop(); // replace bad filename characters with spaces - return newName.replace(kBadFileCharacters, QStringLiteral(" ")); + return newName; } void TrackExportWorker::copyFile(const QFileInfo& source_fileinfo, diff --git a/src/test/file_test.cpp b/src/test/file_test.cpp deleted file mode 100644 index 8f5992790c38..000000000000 --- a/src/test/file_test.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "util/file.h" - -#include - -#include "test/mixxxtest.h" - -class FileTest : public testing::Test { -}; - -TEST_F(FileTest, TestSafeFilename) { - // Generate a file name for the temporary file - const auto fileName = QStringLiteral("broken/ <> :\"\\|ok?*!.mp3"); - const auto expected = QStringLiteral("broken# ## ####ok##!.mp3"); - auto output = FileUtils::safeFilename(fileName); - //qDebug() << "replaced" << output; - ASSERT_EQ(expected, output); - - // test 0 byte characters - const auto fileName0 = QStringLiteral("t2\0\10Z"); - auto output2 = FileUtils::safeFilename(fileName0); - //qDebug() << "replaced" << output2; - ASSERT_EQ(QStringLiteral("t2##Z"), output2); -} diff --git a/src/test/fileutils_test.cpp b/src/test/fileutils_test.cpp new file mode 100644 index 000000000000..9c92ebe0c969 --- /dev/null +++ b/src/test/fileutils_test.cpp @@ -0,0 +1,37 @@ +#include "util/fileutils.h" + +#include + +#include "test/mixxxtest.h" + +class FileUtilsTest : public testing::Test { +}; + +TEST_F(FileUtilsTest, TestSafeFilename) { + // Generate a file name for the temporary file + const auto fileName = QStringLiteral("broken/ <> :\"\\|ok?*!.mp3"); + const auto expected = QStringLiteral("broken/ ## ##\\#ok##!.mp3"); + auto output = FileUtils::safeFilename(fileName); + ASSERT_EQ(expected, output); + + // test 0 byte characters + const auto fileName0 = QStringLiteral("t2\0\10Z"); + auto output2 = FileUtils::safeFilename(fileName0); + ASSERT_EQ(QStringLiteral("t2##Z"), output2); +} + +TEST_F(FileUtilsTest, TestDirReplace) { + // Generate a file name for the temporary file + const auto fileName = QStringLiteral("filename/with\\characters.mp3"); + const auto expected = QStringLiteral("filename-with-characters.mp3"); + auto output = FileUtils::replaceDirChars(fileName); + ASSERT_EQ(expected, output); +} + +TEST_F(FileUtilsTest, TestFileEscape) { + // Generate a file name for the temporary file + const auto fileName = QStringLiteral("A<>ame/with\\chars.mp3"); + const auto expected = QStringLiteral("A##ame-with-chars.mp3"); + auto output = FileUtils::escapeFileName(fileName); + ASSERT_EQ(expected, output); +} diff --git a/src/test/formatter_test.cpp b/src/test/formatter_test.cpp index 51316479418d..3f4e024a1bc8 100644 --- a/src/test/formatter_test.cpp +++ b/src/test/formatter_test.cpp @@ -104,3 +104,15 @@ TEST_F(FormatterTest, TestRoundFilter) { EXPECT_EQ(t2->render(context), QString("1.24")); } + +TEST_F(FormatterTest, TestEscape) { + // Generate a file name for the temporary file + auto engine = Formatter::getEngine(nullptr); + auto context = new Context(); + context->insert(QStringLiteral("file"), QStringLiteral("file<>|with_/&terrible.name")); + context->insert(QStringLiteral("dir"), QStringLiteral("extra/bad\\directory")); + Template t1 = engine->newTemplate(QStringLiteral("{{dir}}/{{file}}"), QStringLiteral("t1")); + + EXPECT_EQ(Formatter::renderFilenameEscape(t1, *context), + QString("extra-bad-directory/file###with_-&terrible.name")); +} diff --git a/src/util/file.cpp b/src/util/file.cpp index 6567ad289d68..99e2055b89ae 100644 --- a/src/util/file.cpp +++ b/src/util/file.cpp @@ -1,12 +1,5 @@ #include "util/file.h" -#include - -namespace { -//const auto kIllegalCharacters = QRegExp("[\0/<>:\"\\|\\?\\*]"); -// see https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names -const auto kIllegalCharacters = QRegExp("([/<>:\"\\\\\\|\\?\\*]|[\x01-\x1F])"); -} // namespace MDir::MDir() { } @@ -36,9 +29,3 @@ MDir& MDir::operator=(const MDir& other) { bool MDir::canAccess() { return Sandbox::canAccessFile(m_dir); } - -QString FileUtils::safeFilename(const QString& input, const QString& replacement) { - auto output = QString(input); - output.replace(kIllegalCharacters, replacement); - return output.replace(QChar::Null, replacement); -} diff --git a/src/util/file.h b/src/util/file.h index 739ee30cb1f9..d1950d0ca95d 100644 --- a/src/util/file.h +++ b/src/util/file.h @@ -33,14 +33,3 @@ class MDir { QDir m_dir; SecurityTokenPointer m_pSecurityToken; }; - -namespace { -const QString kDefaultReplacementCharacter = QString("#"); -} - -class FileUtils { - public: - // returns a filename that is safe on all platforms and does not contain unwanted characters like newline - static QString safeFilename(const QString& input, - const QString& replacement = kDefaultReplacementCharacter); -}; diff --git a/src/util/fileutils.cpp b/src/util/fileutils.cpp new file mode 100644 index 000000000000..68ccb004839f --- /dev/null +++ b/src/util/fileutils.cpp @@ -0,0 +1,30 @@ +#include "util/fileutils.h" + +#include + +namespace { +// see https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names +const auto kIllegalCharacters = QRegExp("([<>:\"\\|\\?\\*]|[\x01-\x1F])"); +const auto kDirChars = QRegExp("[/\\\\]"); +} // namespace + +QString FileUtils::safeFilename(const QString& input, const QString& replacement) { + auto output = QString(input); + output.replace(kIllegalCharacters, replacement); + return output.replace(QChar::Null, replacement); +} + +QString FileUtils::replaceDirChars(const QString& input, const QString& replacement) { + auto output = QString(input); + return output.replace(kDirChars, replacement); +} + +QString FileUtils::escapeFileName( + const QString& input, + const QString& fileReplaceChar, + const QString& dirReplaceChar) { + auto output = QString(input); + output.replace(kDirChars, dirReplaceChar); + output.replace(kIllegalCharacters, fileReplaceChar); + return output.replace(QChar::Null, fileReplaceChar); +} diff --git a/src/util/fileutils.h b/src/util/fileutils.h new file mode 100644 index 000000000000..78f1f67ae0d1 --- /dev/null +++ b/src/util/fileutils.h @@ -0,0 +1,18 @@ +#include + +namespace { +const QString kDefaultFileReplacementCharacter = QString("#"); +const QString kDefaultDirReplacementCharacter = QString("-"); +} // namespace + +class FileUtils { + public: + // returns a filename that is safe on all platforms and does not contain unwanted characters like newline + static QString safeFilename(const QString& input, + const QString& replacement = kDefaultFileReplacementCharacter); + static QString replaceDirChars(const QString& input, + const QString& replacement = kDefaultDirReplacementCharacter); + static QString escapeFileName(const QString& input, + const QString& fileReplaceChar = kDefaultFileReplacementCharacter, + const QString& dirReplaceChar = kDefaultDirReplacementCharacter); +}; diff --git a/src/util/formatter.cpp b/src/util/formatter.cpp index 64a7f538c251..4c8fe80f52da 100644 --- a/src/util/formatter.cpp +++ b/src/util/formatter.cpp @@ -8,23 +8,36 @@ #include #include -class NoEscapeStream : public Grantlee::OutputStream { - public: - NoEscapeStream() - : Grantlee::OutputStream() { - } - NoEscapeStream(QTextStream* stream) - : Grantlee::OutputStream(stream) { - } - ~NoEscapeStream() override{}; - - QString escape(const QString& input) const override { - return input; - } - QSharedPointer clone(QTextStream* stream) const override { - return QSharedPointer::create(stream); - } -}; +#include "util/fileutils.h" + +NoEscapeStream::NoEscapeStream() + : Grantlee::OutputStream() { +} +NoEscapeStream::NoEscapeStream(QTextStream* stream) + : Grantlee::OutputStream(stream) { +} + +NoEscapeStream::~NoEscapeStream(){}; + +QSharedPointer NoEscapeStream::clone(QTextStream* stream) const { + return QSharedPointer::create(stream); +} + +FileEscapeStream::FileEscapeStream() + : Grantlee::OutputStream() { +} +FileEscapeStream::FileEscapeStream(QTextStream* stream) + : Grantlee::OutputStream(stream) { +} +FileEscapeStream::~FileEscapeStream(){}; + +QString FileEscapeStream::escape(const QString& input) const { + return FileUtils::escapeFileName(input); +} + +QSharedPointer FileEscapeStream::clone(QTextStream* stream) const { + return QSharedPointer::create(stream); +} Grantlee::Engine* Formatter::getEngine(QObject* parent) { auto engine = new Grantlee::Engine(parent); @@ -40,3 +53,11 @@ QString Formatter::renderNoEscape(Grantlee::Template& tmpl, Grantlee::Context& c tmpl->render(&os, &context); return rendered; } + +QString Formatter::renderFilenameEscape(Grantlee::Template& tmpl, Grantlee::Context& context) { + auto rendered = QString(); + auto stream = QTextStream(&rendered); + FileEscapeStream os(&stream); + tmpl->render(&os, &context); + return rendered; +} diff --git a/src/util/formatter.h b/src/util/formatter.h index 34595c30ef89..70d7cf177aa5 100644 --- a/src/util/formatter.h +++ b/src/util/formatter.h @@ -4,8 +4,31 @@ #include #include +#include #include +class NoEscapeStream : public Grantlee::OutputStream { + public: + NoEscapeStream(); + NoEscapeStream(QTextStream* stream); + ~NoEscapeStream() override; + + QString escape(const QString& input) const override { + return input; + }; + QSharedPointer clone(QTextStream* stream) const override; +}; + +class FileEscapeStream : public Grantlee::OutputStream { + public: + FileEscapeStream(); + FileEscapeStream(QTextStream* stream); + ~FileEscapeStream() override; + + QString escape(const QString& input) const override; + QSharedPointer clone(QTextStream* stream) const override; +}; + class Formatter { public: static Grantlee::Engine* getEngine(QObject* parent); @@ -13,4 +36,7 @@ class Formatter { static QString renderNoEscape( Grantlee::Template& templ, Grantlee::Context& context); + static QString renderFilenameEscape( + Grantlee::Template& templ, + Grantlee::Context& context); }; diff --git a/src/util/formatterplugin/mixxxformatter.cpp b/src/util/formatterplugin/mixxxformatter.cpp index 53587a4b404d..68cefee08cf7 100644 --- a/src/util/formatterplugin/mixxxformatter.cpp +++ b/src/util/formatterplugin/mixxxformatter.cpp @@ -11,6 +11,7 @@ #include #include "moc_mixxxformatter.cpp" +#include "util/fileutils.h" using namespace Grantlee; @@ -111,6 +112,15 @@ QVariant Rounder::doFilter(const QVariant& input, return SafeString(QString("%1").arg(rValue, 0, 'f', arg)); } +QVariant NoDir::doFilter(const QVariant& input, + const QVariant& argument, + bool autoescape) const { + Q_UNUSED(autoescape) + auto value = getSafeString(input); + + return SafeString(FileUtils::replaceDirChars(value.get())); +} + QHash FormatterPlugin::nodeFactories(const QString& name) { Q_UNUSED(name); QHash nodes; @@ -124,6 +134,7 @@ QHash FormatterPlugin::filters(const QString& name) { filters.insert("rangegroup", new RangeGroup()); filters.insert("zeropad", new ZeroPad()); filters.insert("round", new Rounder()); + filters.insert("nodir", new NoDir()); return filters; } diff --git a/src/util/formatterplugin/mixxxformatter.h b/src/util/formatterplugin/mixxxformatter.h index aeff152f8c23..5581cbe089be 100644 --- a/src/util/formatterplugin/mixxxformatter.h +++ b/src/util/formatterplugin/mixxxformatter.h @@ -52,6 +52,20 @@ class Rounder : public Filter { }; }; +/// SafeFileName +class NoDir : public Filter { + public: + NoDir(){}; + ~NoDir(){}; + + QVariant doFilter(const QVariant& input, + const QVariant& arg = {}, + bool autoescape = false) const override; + bool isSafe() const override { + return true; + }; +}; + class FormatterPlugin : public QObject, public TagLibraryInterface { Q_OBJECT Q_INTERFACES(Grantlee::TagLibraryInterface) From 488671b31f09d1845575dcae84c7855051e66bfb Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Wed, 3 Feb 2021 03:43:51 +0100 Subject: [PATCH 26/36] Use Dropdown Menu for pattern suggestions, not a ComboBox --- src/library/export/dlgtrackexport.ui | 157 ++++++++++++++++---------- src/library/export/trackexportdlg.cpp | 42 +++++-- src/library/export/trackexportdlg.h | 6 + 3 files changed, 138 insertions(+), 67 deletions(-) diff --git a/src/library/export/dlgtrackexport.ui b/src/library/export/dlgtrackexport.ui index e2cc9862776d..ce8838f73268 100644 --- a/src/library/export/dlgtrackexport.ui +++ b/src/library/export/dlgtrackexport.ui @@ -59,6 +59,67 @@ + + + + Pattern + + + + + + + Preview + + + + + + + + + + + + + .m3u8 + + + + + .m3u + + + + + .pls + + + + + + + + + + + + + + + Browse + + + + + + + + + Create Playlist + + + @@ -98,20 +159,6 @@ - - - - - - - - - Browse - - - - - @@ -119,56 +166,31 @@ - - - - Preview - - - - - - true - - - - - - - Pattern - - - - - - - Create Playlist - - - - - + - + - - - - .m3u8 - - - - - .m3u - - - - - .pls - - + + + + + + + 15 + 24 + + + + QToolButton::InstantPopup + + + Qt::ToolButtonIconOnly + + + Qt::DownArrow + @@ -325,6 +347,21 @@ + + tabWidget + folderEdit + browseButton + patternEdit + patternButton + playlistExport + playlistName + startButton + cancelButton + playlistSuffix + pushButton + statusTable + progressCancelButton + diff --git a/src/library/export/trackexportdlg.cpp b/src/library/export/trackexportdlg.cpp index 52ecf795c842..5b7218057adb 100644 --- a/src/library/export/trackexportdlg.cpp +++ b/src/library/export/trackexportdlg.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -28,7 +29,7 @@ const QStringList kDefaultPatterns = QStringList() "}}.{{ track.extension }}" << "{{ crate.name }}/{{ index|zeropad:\"3\" }} - {{ track.artist }}" " - {{ track.title }}.{{ track.extension }}" - << "{{ playlist.name }}/{{ index }} - {{ track.artist }} - " + << "{{ playlist.name }}/{{ index|zeropad:\"3\" }} - {{ track.artist }} - " "{{ track.title }}.{{ track.extension }}"; } // anonymous namespace @@ -45,13 +46,18 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, m_worker(nullptr) { setupUi(this); + QString musicDir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); + auto exportDir = QDir(musicDir + "/Mixxx/Export"); QString lastExportDirectory = m_pConfig->getValue( ConfigKey("[Library]", "LastTrackCopyDirectory"), - QStandardPaths::writableLocation(QStandardPaths::MusicLocation)); + exportDir.absolutePath()); folderEdit->setText(lastExportDirectory); m_worker = new TrackExportWorker(folderEdit->text(), m_tracks, context); + m_patternMenu = new QMenu(patternButton); + m_patternMenu->installEventFilter(this); + if (playlist) { playlistName->setText(*playlist); } @@ -92,12 +98,20 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, Q_UNUSED(x); updatePreview(); }); - connect(comboPattern, - &QComboBox::currentTextChanged, + + patternEdit->setText(kDefaultPatterns[0]); + connect(patternEdit, + &QLineEdit::textChanged, [this](const QString& x) { Q_UNUSED(x); updatePreview(); }); + patternButton->setMenu(m_patternMenu); + + connect(m_patternMenu, + &QMenu::triggered, + this, + &TrackExportDlg::slotPatternSelected); connect(m_worker, &TrackExportWorker::progress, @@ -126,8 +140,22 @@ TrackExportDlg::~TrackExportDlg() { void TrackExportDlg::populateDefaultPatterns() { for (auto pattern : kDefaultPatterns) { - comboPattern->addItem(pattern, QVariant(true)); + m_patternMenu->addAction(pattern); + } +} + +void TrackExportDlg::slotPatternSelected(QAction* action) { + patternEdit->setText(action->text()); +} + +bool TrackExportDlg::eventFilter(QObject* obj, QEvent* event) { + if (event->type() == QEvent::Show && obj == m_patternMenu) { + QPoint pos = m_patternMenu->pos(); + pos.rx() -= m_patternMenu->width() - patternButton->width(); + m_patternMenu->move(pos); + return true; } + return false; } bool TrackExportDlg::browseFolder() { @@ -148,7 +176,7 @@ void TrackExportDlg::closeEvent(QCloseEvent* event) { void TrackExportDlg::setEnableControls(bool enabled) { startButton->setEnabled(enabled); - comboPattern->setEnabled(enabled); + patternEdit->setEnabled(enabled); folderEdit->setEnabled(enabled); browseButton->setEnabled(enabled); playlistName->setEnabled(enabled); @@ -192,7 +220,7 @@ void TrackExportDlg::updatePreview() { VERIFY_OR_DEBUG_ASSERT(!m_tracks.isEmpty()) { return; } - QString pattern = comboPattern->currentText(); + QString pattern = patternEdit->text(); m_worker->setPattern(&pattern); m_worker->setDestDir(folderEdit->text()); previewLabel->setText(m_worker->applyPattern(m_tracks[0], 1)); diff --git a/src/library/export/trackexportdlg.h b/src/library/export/trackexportdlg.h index 6ce461c18881..6e64b97f2a07 100644 --- a/src/library/export/trackexportdlg.h +++ b/src/library/export/trackexportdlg.h @@ -13,6 +13,7 @@ #include "preferences/usersettings.h" #include "track/track_decl.h" +class QMenu; // A dialog for interacting with the track exporter in an interactive manner. // Handles errors and user interactions. class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { @@ -34,6 +35,7 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { const QString* playlistName = nullptr); virtual ~TrackExportDlg(); void open() override; + bool eventFilter(QObject* obj, QEvent* event) override; public slots: void slotProgress(const QString from, const QString to, int progress, int count); @@ -50,6 +52,9 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { protected: bool browseFolder(); + private slots: + void slotPatternSelected(QAction* action); + private: // Called when progress is complete or the procedure has been canceled. // Makes sure the exporter thread has exited. @@ -64,6 +69,7 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { UserSettingsPointer m_pConfig; TrackPointerList m_tracks; TrackExportWorker* m_worker; + QMenu* m_patternMenu; int m_errorCount = 0; int m_skippedCount = 0; int m_okCount = 0; From 4d0ce25264180d0fedcf2d6f6f86d238ce83cd9f Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Wed, 3 Feb 2021 03:47:19 +0100 Subject: [PATCH 27/36] Implement default lookup function for keys (not working) --- src/track/keys.h | 15 +++++++++++++++ src/util/formatter.cpp | 3 +++ 2 files changed, 18 insertions(+) diff --git a/src/track/keys.h b/src/track/keys.h index 5be8ee00780d..8e14f4fe4148 100644 --- a/src/track/keys.h +++ b/src/track/keys.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -71,3 +73,16 @@ inline bool operator!=(const Keys& lhs, const Keys& rhs) { } Q_DECLARE_METATYPE(Keys) + +// FIXME(XXX) This should work according to grentlee docs, but it does not +GRANTLEE_BEGIN_LOOKUP(Keys) +if (property.isEmpty()) { + return object.getOpenkey(); +} +GRANTLEE_END_LOOKUP + +GRANTLEE_BEGIN_LOOKUP_PTR(Keys) +if (property.isEmpty()) { + return object->getOpenkey(); +} +GRANTLEE_END_LOOKUP diff --git a/src/util/formatter.cpp b/src/util/formatter.cpp index 4c8fe80f52da..3b11984234e4 100644 --- a/src/util/formatter.cpp +++ b/src/util/formatter.cpp @@ -8,6 +8,7 @@ #include #include +#include "track/keys.h" #include "util/fileutils.h" NoEscapeStream::NoEscapeStream() @@ -40,6 +41,8 @@ QSharedPointer FileEscapeStream::clone(QTextStream* stre } Grantlee::Engine* Formatter::getEngine(QObject* parent) { + Grantlee::registerMetaType(); + auto engine = new Grantlee::Engine(parent); engine->addDefaultLibrary(QStringLiteral("mixxxformatter")); // register custom From 049c65d6db5e9e5e5f3c3ba2386a494ef8b21d67 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Fri, 5 Feb 2021 02:47:53 +0100 Subject: [PATCH 28/36] fix clazy warnings --- src/library/export/trackexportworker.h | 2 +- src/library/trackset/crate/cratesummary.h | 2 +- src/library/trackset/playlistsummary.h | 6 +++--- src/util/file.cpp | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/library/export/trackexportworker.h b/src/library/export/trackexportworker.h index a2d87d819807..6b7ec10b185c 100644 --- a/src/library/export/trackexportworker.h +++ b/src/library/export/trackexportworker.h @@ -82,7 +82,7 @@ class TrackExportWorker : public QThread { } /// Sets the filename for the playlist to generate - void setPlaylist(QString playlist) { + void setPlaylist(const QString& playlist) { m_playlist = playlist; } /// returns the playlist filename diff --git a/src/library/trackset/crate/cratesummary.h b/src/library/trackset/crate/cratesummary.h index a2d7dd4be1e7..3bb3dbf5ae3b 100644 --- a/src/library/trackset/crate/cratesummary.h +++ b/src/library/trackset/crate/cratesummary.h @@ -58,7 +58,7 @@ class CrateSummaryWrapper : public QObject { QString getName() const { return m_summary.getName(); }; - void setName(QString name) { + void setName(const QString& name) { m_summary.setName(name); }; diff --git a/src/library/trackset/playlistsummary.h b/src/library/trackset/playlistsummary.h index 115bb11c3b7a..d6c872a9f41a 100644 --- a/src/library/trackset/playlistsummary.h +++ b/src/library/trackset/playlistsummary.h @@ -6,7 +6,7 @@ class PlaylistSummary { public: - explicit PlaylistSummary(int id = -1, QString label = nullptr) + explicit PlaylistSummary(int id = -1, const QString& label = nullptr) : m_id(id), m_name(label), m_count(0), @@ -39,7 +39,7 @@ class PlaylistSummary { QString name() const { return m_name; } - void setName(QString name) { + void setName(const QString& name) { m_name = name; } @@ -103,7 +103,7 @@ class PlaylistSummaryWrapper : public QObject { QString getName() const { return m_summary.name(); }; - void setName(QString name) { + void setName(const QString& name) { m_summary.setName(name); }; diff --git a/src/util/file.cpp b/src/util/file.cpp index 99e2055b89ae..962055c2ccfc 100644 --- a/src/util/file.cpp +++ b/src/util/file.cpp @@ -1,6 +1,5 @@ #include "util/file.h" - MDir::MDir() { } From 2118737069d2394355c235ddc08ad80cb30a2f6c Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Mon, 8 Feb 2021 00:42:27 +0100 Subject: [PATCH 29/36] clazy fixes --- src/library/dao/playlistdao.cpp | 10 +++++----- src/library/dao/playlistdao.h | 4 ++-- src/library/export/trackexportdlg.cpp | 10 +++++----- src/library/export/trackexportdlg.h | 6 +++--- src/library/export/trackexportworker.h | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/library/dao/playlistdao.cpp b/src/library/dao/playlistdao.cpp index 613520dda231..5c57da184061 100644 --- a/src/library/dao/playlistdao.cpp +++ b/src/library/dao/playlistdao.cpp @@ -1197,26 +1197,26 @@ void PlaylistDAO::getPlaylistsTrackIsIn(TrackId trackId, } } -QList PlaylistDAO::createPlaylistSummaryForTracks(QList tracks) { +QList PlaylistDAO::createPlaylistSummaryForTracks(const QList& tracks) { QSet allPlaylistIds; QSet playlistIds; QMap trackCount; - for (TrackId trackId : qAsConst(tracks)) { + for (TrackId trackId : tracks) { PlaylistDAO::getPlaylistsTrackIsIn(trackId, &playlistIds); allPlaylistIds += playlistIds; - for (int playlistId : playlistIds) { + for (int playlistId : qAsConst(playlistIds)) { trackCount[playlistId] = trackCount.value(playlistId, 0) + 1; } } QList summaries = PlaylistDAO::createPlaylistSummary(&allPlaylistIds); - for (PlaylistSummary summary : qAsConst(summaries)) { + for (PlaylistSummary summary : summaries) { DEBUG_ASSERT(trackCount.contains(summary.id())); summary.setMatches(trackCount.value(summary.id())); } return summaries; } -QList PlaylistDAO::createPlaylistSummary(QSet* playlistIds) { +QList PlaylistDAO::createPlaylistSummary(const QSet* playlistIds) { QList playlistLabels; // Setup the sidebar playlist model diff --git a/src/library/dao/playlistdao.h b/src/library/dao/playlistdao.h index 1082a48041d4..9d1c925bf6a5 100644 --- a/src/library/dao/playlistdao.h +++ b/src/library/dao/playlistdao.h @@ -118,8 +118,8 @@ class PlaylistDAO : public QObject, public virtual DAO { bool isTrackInPlaylist(TrackId trackId, const int playlistId) const; void getPlaylistsTrackIsIn(TrackId trackId, QSet* playlistSet) const; - QList createPlaylistSummary(QSet* playlistIds = nullptr); - QList createPlaylistSummaryForTracks(QList tracks); + QList createPlaylistSummary(const QSet* playlistIds = nullptr); + QList createPlaylistSummaryForTracks(const QList& tracks); PlaylistSummary getPlaylistSummary(int playlistId); void setAutoDJProcessor(AutoDJProcessor* pAutoDJProcessor); diff --git a/src/library/export/trackexportdlg.cpp b/src/library/export/trackexportdlg.cpp index 5b7218057adb..e227c92bf83a 100644 --- a/src/library/export/trackexportdlg.cpp +++ b/src/library/export/trackexportdlg.cpp @@ -139,7 +139,7 @@ TrackExportDlg::~TrackExportDlg() { } void TrackExportDlg::populateDefaultPatterns() { - for (auto pattern : kDefaultPatterns) { + for (const auto& pattern : kDefaultPatterns) { m_patternMenu->addAction(pattern); } } @@ -227,7 +227,7 @@ void TrackExportDlg::updatePreview() { errorLabel->setText(m_worker->errorMessage()); } -int TrackExportDlg::addStatus(const QString status, const QString to) { +int TrackExportDlg::addStatus(const QString& status, const QString& to) { auto scrollbar = statusTable->verticalScrollBar(); bool atEnd = scrollbar->value() == scrollbar->maximum(); int row = statusTable->rowCount(); @@ -244,7 +244,7 @@ int TrackExportDlg::addStatus(const QString status, const QString to) { return row; } -void TrackExportDlg::slotResult(TrackExportWorker::ExportResult result, const QString msg) { +void TrackExportDlg::slotResult(TrackExportWorker::ExportResult result, const QString& msg) { int type = QTableWidgetItem::UserType; if (result == TrackExportWorker::ExportResult::EXPORT_COMPLETE) { exportProgress->setValue(exportProgress->maximum()); @@ -269,7 +269,7 @@ void TrackExportDlg::slotResult(TrackExportWorker::ExportResult result, const QS statusTable->setItem(row, 2, item); } -void TrackExportDlg::slotProgress(const QString from, const QString to, int progress, int count) { +void TrackExportDlg::slotProgress(const QString& from, const QString& to, int progress, int count) { if (!from.isEmpty() || !to.isEmpty()) { addStatus(from, to); } @@ -333,7 +333,7 @@ void TrackExportDlg::stopWorker() { void TrackExportDlg::open() { bool empty = true; // check if at least one trackpointer is valid - for (TrackPointer track : qAsConst(m_tracks)) { + for (const TrackPointer& track : qAsConst(m_tracks)) { if (track) { empty = false; break; diff --git a/src/library/export/trackexportdlg.h b/src/library/export/trackexportdlg.h index 6e64b97f2a07..3f42c3c8543d 100644 --- a/src/library/export/trackexportdlg.h +++ b/src/library/export/trackexportdlg.h @@ -38,8 +38,8 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { bool eventFilter(QObject* obj, QEvent* event) override; public slots: - void slotProgress(const QString from, const QString to, int progress, int count); - void slotResult(TrackExportWorker::ExportResult result, const QString msg); + void slotProgress(const QString& from, const QString& to, int progress, int count); + void slotResult(TrackExportWorker::ExportResult result, const QString& msg); void slotAskOverwriteMode( const QString& filename, std::promise* promise); @@ -60,7 +60,7 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { // Makes sure the exporter thread has exited. void finish(); void stopWorker(); - int addStatus(const QString status, const QString to); + int addStatus(const QString& status, const QString& to); void updatePreview(); void setEnableControls(bool enabled); void closeEvent(QCloseEvent* event) override; diff --git a/src/library/export/trackexportworker.h b/src/library/export/trackexportworker.h index 6b7ec10b185c..b1a7c56bf55c 100644 --- a/src/library/export/trackexportworker.h +++ b/src/library/export/trackexportworker.h @@ -109,8 +109,8 @@ class TrackExportWorker : public QThread { void askOverwriteMode( const QString& filename, std::promise* promise); - void progress(const QString from, const QString to, int progress, int count); - void result(TrackExportWorker::ExportResult result, const QString msg); + void progress(const QString& from, const QString& to, int progress, int count); + void result(TrackExportWorker::ExportResult result, const QString& msg); void canceled(); private: From 87aa7537a4dfa49555e852608c6188a0dc09f513 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Mon, 8 Feb 2021 00:44:30 +0100 Subject: [PATCH 30/36] Use one button line in export --- src/library/export/dlgtrackexport.ui | 211 ++++++++++++-------------- src/library/export/trackexportdlg.cpp | 7 +- 2 files changed, 96 insertions(+), 122 deletions(-) diff --git a/src/library/export/dlgtrackexport.ui b/src/library/export/dlgtrackexport.ui index ce8838f73268..df453c1b9a40 100644 --- a/src/library/export/dlgtrackexport.ui +++ b/src/library/export/dlgtrackexport.ui @@ -59,13 +59,27 @@ - - + + - Pattern + Create Playlist + + + + + + + + + Browse + + + + + @@ -73,6 +87,20 @@ + + + + Pattern + + + + + + + Folder + + + @@ -99,27 +127,6 @@ - - - - - - - - - Browse - - - - - - - - - Create Playlist - - - @@ -128,17 +135,20 @@ - + 0 0 - + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + Qt::TextSelectableByMouse + @@ -159,13 +169,6 @@ - - - - Folder - - - @@ -189,7 +192,7 @@ Qt::ToolButtonIconOnly - Qt::DownArrow + Qt::NoArrow @@ -197,78 +200,6 @@ - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - - - - 0 - 0 - - - - Help - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - &Close - - - - - - - - 0 - 0 - - - - &Start - - - - - @@ -301,13 +232,6 @@ - - - - &Cancel - - - @@ -345,6 +269,62 @@ + + + + + + + 0 + 0 + + + + Help + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + &Close + + + + + + + + 0 + 0 + + + + &Start + + + + + @@ -355,12 +335,11 @@ patternButton playlistExport playlistName - startButton - cancelButton playlistSuffix pushButton + cancelButton + startButton statusTable - progressCancelButton diff --git a/src/library/export/trackexportdlg.cpp b/src/library/export/trackexportdlg.cpp index e227c92bf83a..777df58c562d 100644 --- a/src/library/export/trackexportdlg.cpp +++ b/src/library/export/trackexportdlg.cpp @@ -68,10 +68,6 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, &QPushButton::clicked, this, &TrackExportDlg::cancelButtonClicked); - connect(progressCancelButton, - &QPushButton::clicked, - this, - &TrackExportDlg::cancelButtonClicked); connect(startButton, &QPushButton::clicked, this, @@ -182,6 +178,7 @@ void TrackExportDlg::setEnableControls(bool enabled) { playlistName->setEnabled(enabled); playlistExport->setEnabled(enabled); playlistSuffix->setEnabled(enabled); + patternButton->setEnabled(enabled); } void TrackExportDlg::slotStartExport() { @@ -204,7 +201,6 @@ void TrackExportDlg::slotStartExport() { tabWidget->setCurrentIndex(1); cancelButton->setText(tr("&Cancel")); - progressCancelButton->setText(tr("&Cancel")); // enable playlist export if (playlistExport->isChecked()) { @@ -327,7 +323,6 @@ void TrackExportDlg::stopWorker() { m_worker->wait(); setEnableControls(true); cancelButton->setText(tr("&Close")); - progressCancelButton->setText(tr("&Close")); } void TrackExportDlg::open() { From 49aa05dec97922e454b5f1a58ed1dd7f80495cb4 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Mon, 8 Feb 2021 00:45:14 +0100 Subject: [PATCH 31/36] Escape playlist name in menu --- src/library/export/trackexportdlg.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/library/export/trackexportdlg.cpp b/src/library/export/trackexportdlg.cpp index 777df58c562d..f64729b739e9 100644 --- a/src/library/export/trackexportdlg.cpp +++ b/src/library/export/trackexportdlg.cpp @@ -9,6 +9,7 @@ #include "moc_trackexportdlg.cpp" #include "util/assert.h" +#include "util/fileutils.h" namespace { @@ -59,7 +60,8 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, m_patternMenu->installEventFilter(this); if (playlist) { - playlistName->setText(*playlist); + // use the escaped version as suggestion for playlist name + playlistName->setText(FileUtils::escapeFileName(*playlist)); } populateDefaultPatterns(); From 1dd67eb5b2ef1f384251c17338bd93c43b61299a Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Fri, 19 Feb 2021 00:42:18 +0100 Subject: [PATCH 32/36] Prevent crashes on patterns like "{{ }}" --- src/library/export/trackexportworker.cpp | 40 +++++++++++++++--------- src/library/export/trackexportworker.h | 1 + src/test/fileutils_test.cpp | 4 +-- src/test/formatter_test.cpp | 4 +-- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/library/export/trackexportworker.cpp b/src/library/export/trackexportworker.cpp index 8531080c42b1..7aa7467a75c3 100644 --- a/src/library/export/trackexportworker.cpp +++ b/src/library/export/trackexportworker.cpp @@ -30,6 +30,7 @@ const auto kResultCantCreateFile = QStringLiteral("Could not create file"); const auto kDefaultPattern = QStringLiteral( "{{ track.basename }}{% if dup %}-{{dup}}{% endif %}" ".{{track.extension}}"); +const auto kEmptyMsg = QLatin1String(""); } // namespace TrackExportWorker::TrackExportWorker(const QString& destDir, @@ -120,31 +121,42 @@ void TrackExportWorker::setPattern(QString* pattern) { if (pattern == nullptr) { m_pattern = nullptr; if (!m_template.isNull()) { + m_template_valid = false; m_template.reset(); } return; } if (!m_engine) { m_engine = Formatter::getEngine(this); - m_engine->setSmartTrimEnabled(true); + // smartTrimEnabled would be good, but causes crashes on invalid pattern '{{ }}' + // m_engine->setSmartTrimEnabled(true); } m_pattern = pattern; updateTemplate(); } + void TrackExportWorker::updateTemplate() { - QString tmpl = m_destDir + QDir::separator().toLatin1() + - (m_pattern ? *m_pattern : kDefaultPattern); - m_template = m_engine->newTemplate(tmpl, QStringLiteral("export")); + QString fullPattern; + if (m_pattern) { + QString trimmed = m_pattern->trimmed(); + fullPattern = m_destDir + QDir::separator().toLatin1() + trimmed; + } else { + fullPattern = m_destDir + QDir::separator().toLatin1() + kDefaultPattern; + } + m_template = m_engine->newTemplate(fullPattern, QStringLiteral("export")); if (m_template->error()) { m_errorMessage = m_template->errorString(); + m_template_valid = false; } else { m_errorMessage = QString(); + m_template_valid = true; } } void TrackExportWorker::run() { m_running = true; m_bStop = false; + m_overwriteMode = OverwriteMode::ASK; int i = 0; auto skippedTracks = TrackPointerList(); QMap copy_list = createCopylist(m_tracks, &skippedTracks); @@ -154,7 +166,7 @@ void TrackExportWorker::run() { jobsTotal++; } - for (TrackPointer track : qAsConst(skippedTracks)) { + for (const TrackPointer& track : qAsConst(skippedTracks)) { QString fileName = track->fileName(); emit progress(fileName, nullptr, 0, jobsTotal); emit result(TrackExportWorker::ExportResult::SKIPPED, kResultEmptyPattern); @@ -178,10 +190,10 @@ void TrackExportWorker::run() { } if (!m_playlist.isEmpty()) { const auto targetDir = QDir(m_destDir); - const QString plsPath = targetDir.filePath(m_playlist); + const QString plsPath = targetDir.filePath(FileUtils::escapeFileName(m_playlist)); QFileInfo plsPathFileinfo(plsPath); - emit progress(QStringLiteral("export playlist"), m_playlist, i, jobsTotal); + emit progress(QStringLiteral("export playlist"), plsPath, i, jobsTotal); QDir plsDir = plsPathFileinfo.absoluteDir(); if (!plsDir.mkpath(plsDir.absolutePath())) { @@ -208,7 +220,7 @@ void TrackExportWorker::run() { i++; } - emit progress(QStringLiteral(""), QStringLiteral(""), i, jobsTotal); + emit progress(kEmptyMsg, kEmptyMsg, i, jobsTotal); emit result(TrackExportWorker::ExportResult::EXPORT_COMPLETE, kResultOk); m_running = false; } @@ -223,19 +235,19 @@ QString TrackExportWorker::applyPattern( TrackPointer track, int index, int duplicateCounter) { - VERIFY_OR_DEBUG_ASSERT(!m_destDir.isEmpty()) { - qWarning() << "empty target directory"; + if (!m_template_valid) { return QString(); } - VERIFY_OR_DEBUG_ASSERT(!m_template.isNull()) { - qWarning() << "template missing"; + VERIFY_OR_DEBUG_ASSERT(!m_destDir.isEmpty()) { + qWarning() << "empty target directory"; return QString(); } VERIFY_OR_DEBUG_ASSERT(m_engine) { qWarning() << "engine missing"; return QString(); } - // fill the context with the proper variables + + // fill the context with the proper variables. m_context->push(); m_context->insert(QStringLiteral("directory"), m_destDir); // this is safe since the context stack is popped after rendering @@ -249,7 +261,7 @@ QString TrackExportWorker::applyPattern( m_context->pop(); // replace bad filename characters with spaces - return newName; + return newName.trimmed(); } void TrackExportWorker::copyFile(const QFileInfo& source_fileinfo, diff --git a/src/library/export/trackexportworker.h b/src/library/export/trackexportworker.h index b1a7c56bf55c..5df2d74de871 100644 --- a/src/library/export/trackexportworker.h +++ b/src/library/export/trackexportworker.h @@ -141,6 +141,7 @@ class TrackExportWorker : public QThread { QString* m_pattern; Grantlee::Context* m_context; Grantlee::Template m_template; + bool m_template_valid{false}; Grantlee::Engine* m_engine{nullptr}; QString m_playlist; }; diff --git a/src/test/fileutils_test.cpp b/src/test/fileutils_test.cpp index 9c92ebe0c969..89205e702f14 100644 --- a/src/test/fileutils_test.cpp +++ b/src/test/fileutils_test.cpp @@ -22,8 +22,8 @@ TEST_F(FileUtilsTest, TestSafeFilename) { TEST_F(FileUtilsTest, TestDirReplace) { // Generate a file name for the temporary file - const auto fileName = QStringLiteral("filename/with\\characters.mp3"); - const auto expected = QStringLiteral("filename-with-characters.mp3"); + const auto fileName = QStringLiteral("filename/with\\chara-cters.mp3"); + const auto expected = QStringLiteral("filename-with-chara-cters.mp3"); auto output = FileUtils::replaceDirChars(fileName); ASSERT_EQ(expected, output); } diff --git a/src/test/formatter_test.cpp b/src/test/formatter_test.cpp index 3f4e024a1bc8..05343f8bb492 100644 --- a/src/test/formatter_test.cpp +++ b/src/test/formatter_test.cpp @@ -109,10 +109,10 @@ TEST_F(FormatterTest, TestEscape) { // Generate a file name for the temporary file auto engine = Formatter::getEngine(nullptr); auto context = new Context(); - context->insert(QStringLiteral("file"), QStringLiteral("file<>|with_/&terrible.name")); + context->insert(QStringLiteral("file"), QStringLiteral("file<>|with_/&terr-ible.name")); context->insert(QStringLiteral("dir"), QStringLiteral("extra/bad\\directory")); Template t1 = engine->newTemplate(QStringLiteral("{{dir}}/{{file}}"), QStringLiteral("t1")); EXPECT_EQ(Formatter::renderFilenameEscape(t1, *context), - QString("extra-bad-directory/file###with_-&terrible.name")); + QString("extra-bad-directory/file###with_-&terr-ible.name")); } From 82652f074ae90737e3532d86b4c5ef92342725c1 Mon Sep 17 00:00:00 2001 From: Daniel Poelzleithner Date: Fri, 19 Feb 2021 02:22:21 +0100 Subject: [PATCH 33/36] Go back to a QComboBox but with custom item handling --- src/library/export/dlgtrackexport.ui | 40 ++++---------- src/library/export/trackexportdlg.cpp | 79 +++++++++++++++++++-------- src/library/export/trackexportdlg.h | 7 ++- 3 files changed, 70 insertions(+), 56 deletions(-) diff --git a/src/library/export/dlgtrackexport.ui b/src/library/export/dlgtrackexport.ui index df453c1b9a40..1192233d74ba 100644 --- a/src/library/export/dlgtrackexport.ui +++ b/src/library/export/dlgtrackexport.ui @@ -38,7 +38,7 @@ - 1 + 0 @@ -170,33 +170,14 @@ - - - - - - - - - - - - 15 - 24 - - - - QToolButton::InstantPopup - - - Qt::ToolButtonIconOnly - - - Qt::NoArrow - - - - + + + true + + + true + + @@ -331,8 +312,7 @@ tabWidget folderEdit browseButton - patternEdit - patternButton + patternCombo playlistExport playlistName playlistSuffix diff --git a/src/library/export/trackexportdlg.cpp b/src/library/export/trackexportdlg.cpp index f64729b739e9..e848ee706e7f 100644 --- a/src/library/export/trackexportdlg.cpp +++ b/src/library/export/trackexportdlg.cpp @@ -56,9 +56,6 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, m_worker = new TrackExportWorker(folderEdit->text(), m_tracks, context); - m_patternMenu = new QMenu(patternButton); - m_patternMenu->installEventFilter(this); - if (playlist) { // use the escaped version as suggestion for playlist name playlistName->setText(FileUtils::escapeFileName(*playlist)); @@ -97,17 +94,18 @@ TrackExportDlg::TrackExportDlg(QWidget* parent, updatePreview(); }); - patternEdit->setText(kDefaultPatterns[0]); - connect(patternEdit, - &QLineEdit::textChanged, + connect(patternCombo, + &QComboBox::currentTextChanged, [this](const QString& x) { Q_UNUSED(x); updatePreview(); }); - patternButton->setMenu(m_patternMenu); - - connect(m_patternMenu, - &QMenu::triggered, + connect(patternCombo, + &QComboBox::editTextChanged, + this, + &TrackExportDlg::slotPatternEdited); + connect(patternCombo, + QOverload::of(&QComboBox::activated), this, &TrackExportDlg::slotPatternSelected); @@ -138,22 +136,58 @@ TrackExportDlg::~TrackExportDlg() { void TrackExportDlg::populateDefaultPatterns() { for (const auto& pattern : kDefaultPatterns) { - m_patternMenu->addAction(pattern); + patternCombo->addItem(pattern, true); } } -void TrackExportDlg::slotPatternSelected(QAction* action) { - patternEdit->setText(action->text()); +void TrackExportDlg::removeDups(const QVariant& data) { + for (int i = 0; i < patternCombo->count(); i++) { + if (patternCombo->itemData(i, Qt::DisplayRole) == data) { + // if the the item was user entered and is exactly the same, + // remove the old one + if (patternCombo->itemData(i, Qt::UserRole).toBool() == false) { + patternCombo->removeItem(i); + qDebug() << "remove i" << i; + i--; + } + } + } } -bool TrackExportDlg::eventFilter(QObject* obj, QEvent* event) { - if (event->type() == QEvent::Show && obj == m_patternMenu) { - QPoint pos = m_patternMenu->pos(); - pos.rx() -= m_patternMenu->width() - patternButton->width(); - m_patternMenu->move(pos); - return true; +void TrackExportDlg::slotPatternSelected(int index) { + QVariant data = patternCombo->itemData(index, Qt::DisplayRole); + removeDups(data); + patternCombo->insertItem(0, data.toString(), false); + patternCombo->setCurrentIndex(0); + m_patternComboSwitched = true; + // we can also remove the item 1 if it is not fixed and a fixed entry exists + QVariant oldEdit = patternCombo->itemData(1, Qt::DisplayRole); + bool found = false; + for (int i = patternCombo->count() - 1; i > 1; i--) { + if (patternCombo->itemData(i, Qt::DisplayRole) == oldEdit) { + found = true; + break; + } + } + if (found && !patternCombo->itemData(1, Qt::UserRole).toBool()) { + patternCombo->removeItem(1); + } +} + +void TrackExportDlg::slotPatternEdited(const QString& text) { + if (m_patternComboSwitched) { + m_patternComboSwitched = false; + return; + } + // in case the user tries to edit a protected pattern, make a copy first + if (patternCombo->currentIndex() == 0) { + bool fixed = patternCombo->itemData(0, Qt::UserRole).toBool(); + if (fixed) { + patternCombo->insertItem(0, text, false); + patternCombo->setCurrentIndex(0); + } + patternCombo->setItemData(0, text, Qt::DisplayRole); } - return false; } bool TrackExportDlg::browseFolder() { @@ -174,13 +208,12 @@ void TrackExportDlg::closeEvent(QCloseEvent* event) { void TrackExportDlg::setEnableControls(bool enabled) { startButton->setEnabled(enabled); - patternEdit->setEnabled(enabled); + patternCombo->setEnabled(enabled); folderEdit->setEnabled(enabled); browseButton->setEnabled(enabled); playlistName->setEnabled(enabled); playlistExport->setEnabled(enabled); playlistSuffix->setEnabled(enabled); - patternButton->setEnabled(enabled); } void TrackExportDlg::slotStartExport() { @@ -218,7 +251,7 @@ void TrackExportDlg::updatePreview() { VERIFY_OR_DEBUG_ASSERT(!m_tracks.isEmpty()) { return; } - QString pattern = patternEdit->text(); + QString pattern = patternCombo->currentText(); m_worker->setPattern(&pattern); m_worker->setDestDir(folderEdit->text()); previewLabel->setText(m_worker->applyPattern(m_tracks[0], 1)); diff --git a/src/library/export/trackexportdlg.h b/src/library/export/trackexportdlg.h index 3f42c3c8543d..ede6582d3950 100644 --- a/src/library/export/trackexportdlg.h +++ b/src/library/export/trackexportdlg.h @@ -35,7 +35,6 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { const QString* playlistName = nullptr); virtual ~TrackExportDlg(); void open() override; - bool eventFilter(QObject* obj, QEvent* event) override; public slots: void slotProgress(const QString& from, const QString& to, int progress, int count); @@ -53,7 +52,8 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { bool browseFolder(); private slots: - void slotPatternSelected(QAction* action); + void slotPatternSelected(int index); + void slotPatternEdited(const QString& text); private: // Called when progress is complete or the procedure has been canceled. @@ -65,12 +65,13 @@ class TrackExportDlg : public QDialog, public Ui::DlgTrackExport { void setEnableControls(bool enabled); void closeEvent(QCloseEvent* event) override; void populateDefaultPatterns(); + void removeDups(const QVariant& data); UserSettingsPointer m_pConfig; TrackPointerList m_tracks; TrackExportWorker* m_worker; - QMenu* m_patternMenu; int m_errorCount = 0; int m_skippedCount = 0; int m_okCount = 0; + bool m_patternComboSwitched = 0; }; From 6f70c446d7c2f5d4dc618426384227a798daaa9e Mon Sep 17 00:00:00 2001 From: Joerg Date: Sun, 24 Sep 2023 12:10:54 +0200 Subject: [PATCH 34/36] Fix handling of windows drive letters in paths (colon is allowed here) --- src/test/fileutils_test.cpp | 2 +- src/util/fileutils.cpp | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/test/fileutils_test.cpp b/src/test/fileutils_test.cpp index 89205e702f14..3cb1cd79ff5c 100644 --- a/src/test/fileutils_test.cpp +++ b/src/test/fileutils_test.cpp @@ -10,7 +10,7 @@ class FileUtilsTest : public testing::Test { TEST_F(FileUtilsTest, TestSafeFilename) { // Generate a file name for the temporary file const auto fileName = QStringLiteral("broken/ <> :\"\\|ok?*!.mp3"); - const auto expected = QStringLiteral("broken/ ## ##\\#ok##!.mp3"); + const auto expected = QStringLiteral("broken/ ## ##/#ok##!.mp3"); auto output = FileUtils::safeFilename(fileName); ASSERT_EQ(expected, output); diff --git a/src/util/fileutils.cpp b/src/util/fileutils.cpp index ba2c9dfba925..bb8c5335e321 100644 --- a/src/util/fileutils.cpp +++ b/src/util/fileutils.cpp @@ -4,13 +4,23 @@ namespace { // see https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names +// Note, that the colon can be part of Windows paths starting with drive letters: C: const auto kIllegalCharacters = QRegularExpression("([<>:\"\\|\\?\\*]|[\x01-\x1F])"); const auto kDirChars = QRegularExpression("[/\\\\]"); } // namespace QString FileUtils::safeFilename(const QString& input, const QString& replacement) { auto output = QString(input); + output.replace(kDirChars, "/"); + bool windowsDriveLetter = false; + if (output.size() > 2 && output[0].toUpper() >= 'A' && + output[0].toUpper() <= 'Z' && output[1] == ':') { + windowsDriveLetter = true; + } output.replace(kIllegalCharacters, replacement); + if (windowsDriveLetter) { + output[1] = ':'; + } return output.replace(QChar::Null, replacement); } @@ -25,6 +35,16 @@ QString FileUtils::escapeFileName( const QString& dirReplaceChar) { auto output = QString(input); output.replace(kDirChars, dirReplaceChar); + + bool windowsDriveLetter = false; + if (output.size() > 2 && output[0].toUpper() >= 'A' && + output[0].toUpper() <= 'Z' && output[1] == ':') { + windowsDriveLetter = true; + } output.replace(kIllegalCharacters, fileReplaceChar); + if (windowsDriveLetter) { + output[1] = ':'; + } + return output.replace(QChar::Null, fileReplaceChar); } From 6e43d320b54b2a7537c04f64d642f7aca464b959 Mon Sep 17 00:00:00 2001 From: Joerg Date: Sun, 24 Sep 2023 12:11:56 +0200 Subject: [PATCH 35/36] Fixed trackexport_test.cpp --- src/test/trackexport_test.cpp | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/test/trackexport_test.cpp b/src/test/trackexport_test.cpp index a76e40a97335..fb9d4b84cf6e 100644 --- a/src/test/trackexport_test.cpp +++ b/src/test/trackexport_test.cpp @@ -58,7 +58,10 @@ TEST_F(TrackExporterTest, SimpleListExport) { tracks.append(track1); tracks.append(track2); tracks.append(track3); + auto pattern = QStringLiteral( + "{{ track.fileName }}"); TrackExportWorker worker(m_exportDir.canonicalPath(), tracks); + worker.setPattern(&pattern); m_answerer.reset(new FakeOverwriteAnswerer(&worker)); worker.run(); @@ -95,7 +98,10 @@ TEST_F(TrackExporterTest, OverwriteSkip) { TrackPointerList tracks; tracks.append(track1); tracks.append(track2); + auto pattern = QStringLiteral( + "{{ track.fileName }}"); TrackExportWorker worker(m_exportDir.canonicalPath(), tracks); + worker.setPattern(&pattern); m_answerer.reset(new FakeOverwriteAnswerer(&worker)); m_answerer->setAnswer(QFileInfo(file1).canonicalFilePath(), TrackExportWorker::OverwriteAnswer::OVERWRITE); @@ -141,7 +147,10 @@ TEST_F(TrackExporterTest, OverwriteAll) { TrackPointerList tracks; tracks.append(track1); tracks.append(track2); + auto pattern = QStringLiteral( + "{{ track.fileName }}"); TrackExportWorker worker(m_exportDir.canonicalPath(), tracks); + worker.setPattern(&pattern); m_answerer.reset(new FakeOverwriteAnswerer(&worker)); m_answerer->setAnswer(QFileInfo(file2).canonicalFilePath(), TrackExportWorker::OverwriteAnswer::OVERWRITE_ALL); @@ -182,7 +191,10 @@ TEST_F(TrackExporterTest, SkipAll) { TrackPointerList tracks; tracks.append(track1); tracks.append(track2); + auto pattern = QStringLiteral( + "{{ track.fileName }}"); TrackExportWorker worker(m_exportDir.canonicalPath(), tracks); + worker.setPattern(&pattern); m_answerer.reset(new FakeOverwriteAnswerer(&worker)); m_answerer->setAnswer(QFileInfo(file2).canonicalFilePath(), TrackExportWorker::OverwriteAnswer::SKIP_ALL); @@ -221,7 +233,10 @@ TEST_F(TrackExporterTest, Cancel) { TrackPointerList tracks; tracks.append(track1); tracks.append(track2); + auto pattern = QStringLiteral( + "{{ track.fileName }}"); TrackExportWorker worker(m_exportDir.canonicalPath(), tracks); + worker.setPattern(&pattern); m_answerer.reset(new FakeOverwriteAnswerer(&worker)); m_answerer->setAnswer(QFileInfo(file2).canonicalFilePath(), TrackExportWorker::OverwriteAnswer::CANCEL); @@ -252,7 +267,10 @@ TEST_F(TrackExporterTest, DedupeList) { TrackPointerList tracks; tracks.append(track1); tracks.append(track2); + auto pattern = QStringLiteral( + "{{ track.fileName }}"); TrackExportWorker worker(m_exportDir.canonicalPath(), tracks); + worker.setPattern(&pattern); m_answerer.reset(new FakeOverwriteAnswerer(&worker)); worker.run(); @@ -289,7 +307,11 @@ TEST_F(TrackExporterTest, MungeFilename) { TrackPointerList tracks; tracks.append(track1); tracks.append(track2); + auto pattern = QStringLiteral( + "{{track.baseName}}{% if dup %}-{{dup|zeropad:\"4\"}}{% endif %}" + ".{{track.extension}}"); TrackExportWorker worker(m_exportDir.canonicalPath(), tracks); + worker.setPattern(&pattern); m_answerer.reset(new FakeOverwriteAnswerer(&worker)); worker.run(); @@ -324,7 +346,7 @@ TEST_F(TrackExporterTest, PatternExport) { tracks.append(track2); tracks.append(track3); auto context = new Grantlee::Context(); - context->insert("t", "t42/"); + context->insert("t", "t42-"); auto pattern = QStringLiteral( "{{t}}{{track.baseName}}-{{track.extension}}-" "{{track.bpm}}{% if index %}-{{index}}{%endif%}#{{ dup }}"); @@ -332,14 +354,17 @@ TEST_F(TrackExporterTest, PatternExport) { worker.setPattern(&pattern); EXPECT_EQ(worker.generateFilename(track1, 0), - QStringLiteral("t42/cover-test-ogg-0#0")); + QString(m_exportDir.canonicalPath()) + .append("/t42-cover-test-ogg-0#0")); EXPECT_EQ(worker.generateFilename(track2, 1), - QStringLiteral("t42/cover-test-flac-0-1#0")); + QString(m_exportDir.canonicalPath()) + .append("/t42-cover-test-flac-0-1#0")); EXPECT_EQ(worker.generateFilename(track3, 0, 23), - QStringLiteral("t42/cover-test-itunes-12-m4a-0#23")); + QString(m_exportDir.canonicalPath()) + .append("/t42-cover-test-itunes-12-m4a-0#23")); auto pattern2 = QStringLiteral("{{track.fileName}}"); worker.setPattern(&pattern2); EXPECT_EQ(worker.generateFilename(track1, 0), - QStringLiteral("cover-test.ogg")); + QString(m_exportDir.canonicalPath()).append("/cover-test.ogg")); } From a3f9ab99908e8d5334df1d67c504d19f6ee34180 Mon Sep 17 00:00:00 2001 From: Joerg Date: Sun, 24 Sep 2023 16:47:02 +0200 Subject: [PATCH 36/36] Fix pre-commit --- src/library/export/trackexportworker.cpp | 2 +- src/library/parser.cpp | 34 +++++++++++------------ src/test/formatter_test.cpp | 6 ++-- src/util/fileutils.cpp | 6 ++-- src/util/fileutils.h | 3 +- src/util/formatterplugin/mixxxformatter.h | 2 +- 6 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/library/export/trackexportworker.cpp b/src/library/export/trackexportworker.cpp index f42e46d30a1d..69bca323166b 100644 --- a/src/library/export/trackexportworker.cpp +++ b/src/library/export/trackexportworker.cpp @@ -82,7 +82,7 @@ QMap TrackExportWorker::createCopylist(const TrackPoin const auto fileName = fileInfo.fileName(); QString destFileName = generateFilename(pTrack, index, 0); if (destFileName.isEmpty()) { - //qWarning() << "pattern generated empty filename for:" << it; + // qWarning() << "pattern generated empty filename for:" << it; skippedTracks->append(pTrack); continue; } diff --git a/src/library/parser.cpp b/src/library/parser.cpp index 504dfe039918..1e34277fdf45 100644 --- a/src/library/parser.cpp +++ b/src/library/parser.cpp @@ -182,27 +182,27 @@ bool Parser::exportPlaylistItemsIntoFile( playlistItemLocations, useRelativePath); } else { - //default export to M3U if file extension is missing + // default export to M3U if file extension is missing if (!playlistFilePath.endsWith( QStringLiteral(".m3u"), Qt::CaseInsensitive)) { - kLogger.debug() - << "No valid file extension for playlist export specified." - << "Appending .m3u and exporting to M3U."; - playlistFilePath.append(QStringLiteral(".m3u")); - if (QFileInfo::exists(playlistFilePath)) { - auto overwrite = QMessageBox::question(nullptr, - QObject::tr("Overwrite File?"), - QObject::tr("A playlist file with the name \"%1\" " - "already exists.\n" - "The default \"m3u\" extension was added " - "because none was specified.\n\n" - "Do you really want to overwrite it?") - .arg(playlistFilePath)); - if (overwrite != QMessageBox::StandardButton::Yes) { - return false; + kLogger.debug() + << "No valid file extension for playlist export specified." + << "Appending .m3u and exporting to M3U."; + playlistFilePath.append(QStringLiteral(".m3u")); + if (QFileInfo::exists(playlistFilePath)) { + auto overwrite = QMessageBox::question(nullptr, + QObject::tr("Overwrite File?"), + QObject::tr("A playlist file with the name \"%1\" " + "already exists.\n" + "The default \"m3u\" extension was added " + "because none was specified.\n\n" + "Do you really want to overwrite it?") + .arg(playlistFilePath)); + if (overwrite != QMessageBox::StandardButton::Yes) { + return false; + } } - } } return ParserM3u::writeM3UFile( playlistFilePath, diff --git a/src/test/formatter_test.cpp b/src/test/formatter_test.cpp index 05343f8bb492..f990512788e0 100644 --- a/src/test/formatter_test.cpp +++ b/src/test/formatter_test.cpp @@ -14,7 +14,7 @@ TEST_F(FormatterTest, TestRangeGroupFilter) { // Generate a file name for the temporary file auto engine = Formatter::getEngine(nullptr); auto context = new Context(); - //context->insert("x1", "122"); + // context->insert("x1", "122"); Template t1 = engine->newTemplate(QStringLiteral("{{143|rangegroup}}"), QStringLiteral("t1")); auto pattern = t1->render(context); @@ -59,7 +59,7 @@ TEST_F(FormatterTest, TestZeropadFilter) { // Generate a file name for the temporary file auto engine = Formatter::getEngine(nullptr); auto context = new Context(); - //context->insert("x1", "122"); + // context->insert("x1", "122"); Template t1 = engine->newTemplate(QStringLiteral("{{1|zeropad}}"), QStringLiteral("t1")); EXPECT_EQ(t1->render(context), @@ -81,7 +81,7 @@ TEST_F(FormatterTest, TestRoundFilter) { // Generate a file name for the temporary file auto engine = Formatter::getEngine(nullptr); auto context = new Context(); - //context->insert("x1", "122"); + // context->insert("x1", "122"); Template t1 = engine->newTemplate(QStringLiteral("{{x1|round}}"), QStringLiteral("t1")); context->insert("x1", QVariant(1.49)); EXPECT_EQ(t1->render(context), diff --git a/src/util/fileutils.cpp b/src/util/fileutils.cpp index bb8c5335e321..1347b7fa07a0 100644 --- a/src/util/fileutils.cpp +++ b/src/util/fileutils.cpp @@ -3,8 +3,10 @@ #include namespace { -// see https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names -// Note, that the colon can be part of Windows paths starting with drive letters: C: +// see +// https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names +// Note, that the colon can be part of Windows paths starting with drive +// letters: C: const auto kIllegalCharacters = QRegularExpression("([<>:\"\\|\\?\\*]|[\x01-\x1F])"); const auto kDirChars = QRegularExpression("[/\\\\]"); } // namespace diff --git a/src/util/fileutils.h b/src/util/fileutils.h index 78f1f67ae0d1..55cc064c6d29 100644 --- a/src/util/fileutils.h +++ b/src/util/fileutils.h @@ -7,7 +7,8 @@ const QString kDefaultDirReplacementCharacter = QString("-"); class FileUtils { public: - // returns a filename that is safe on all platforms and does not contain unwanted characters like newline + // returns a filename that is safe on all platforms and does not contain + // unwanted characters like newline static QString safeFilename(const QString& input, const QString& replacement = kDefaultFileReplacementCharacter); static QString replaceDirChars(const QString& input, diff --git a/src/util/formatterplugin/mixxxformatter.h b/src/util/formatterplugin/mixxxformatter.h index 5581cbe089be..77ec5385ab87 100644 --- a/src/util/formatterplugin/mixxxformatter.h +++ b/src/util/formatterplugin/mixxxformatter.h @@ -38,7 +38,7 @@ class ZeroPad : public Filter { }; }; -/// Rounds a double to n precission (default = 0) +/// Rounds a double to n precision (default = 0) class Rounder : public Filter { public: Rounder(){};