ACMX 2.139.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
audio-playlist.cpp
Go to the documentation of this file.
1#include "audio-playlist.hpp"
2#include "custom_style.hpp"
3
4#include <QAbstractItemModel>
5#include <QDir>
6#include <QFile>
7#include <QFileDialog>
8#include <QFileInfo>
9#include <QHBoxLayout>
10#include <QLabel>
11#include <QListWidget>
12#include <QMessageBox>
13#include <QPushButton>
14#include <QRandomGenerator>
15#include <QSaveFile>
16#include <QSettings>
17#include <QVBoxLayout>
18
19#include <algorithm>
20#include <functional>
21#include <random>
22#include <vector>
23
24namespace {
25
26 constexpr int TRACK_PATH_ROLE = Qt::UserRole;
27
28 bool isUrl(const QString &path) { return path.contains("://"); }
29
30 QString displayName(const QString &path) {
31 if (isUrl(path))
32 return path;
33 const QString name = QFileInfo(path).fileName();
34 return name.isEmpty() ? path : name;
35 }
36
37} // namespace
38
39AudioPlaylistDialog::AudioPlaylistDialog(const QString &playlistPath, QWidget *parent) : QDialog(parent) {
40 setupUi();
41 if (!playlistPath.isEmpty() && QFileInfo::exists(playlistPath))
44}
45
47
49 setWindowTitle("Audio M3U Playlist Editor");
50 setMinimumSize(720, 520);
51
52 QVBoxLayout *mainLayout = new QVBoxLayout(this);
53 QLabel *instructions = new QLabel("Add audio tracks in playback order. You can reorder, sort, shuffle, "
54 "open an existing M3U playlist, or save the current list as M3U.",
55 this);
56 instructions->setWordWrap(true);
57 mainLayout->addWidget(instructions);
58
59 pathLabel = new QLabel(this);
60 pathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
61 pathLabel->setWordWrap(true);
62 mainLayout->addWidget(pathLabel);
63
64 trackList = new QListWidget(this);
65 trackList->setSelectionMode(QAbstractItemView::ExtendedSelection);
66 trackList->setDragDropMode(QAbstractItemView::InternalMove);
67 trackList->setDefaultDropAction(Qt::MoveAction);
68 mainLayout->addWidget(trackList, 1);
69
70 QHBoxLayout *trackButtons = new QHBoxLayout();
71 QPushButton *addButton = new QPushButton("Add Tracks...", this);
72 removeButton = new QPushButton("Remove", this);
73 upButton = new QPushButton("Move Up", this);
74 downButton = new QPushButton("Move Down", this);
75 sortButton = new QPushButton("Sort", this);
76 shuffleButton = new QPushButton("Shuffle", this);
77 clearButton = new QPushButton("Clear", this);
78 trackButtons->addWidget(addButton);
79 trackButtons->addWidget(removeButton);
80 trackButtons->addWidget(upButton);
81 trackButtons->addWidget(downButton);
82 trackButtons->addWidget(sortButton);
83 trackButtons->addWidget(shuffleButton);
84 trackButtons->addWidget(clearButton);
85 mainLayout->addLayout(trackButtons);
86
87 QHBoxLayout *fileButtons = new QHBoxLayout();
88 QPushButton *openButton = new QPushButton("Open M3U...", this);
89 saveButton = new QPushButton("Save", this);
90 QPushButton *saveAsButton = new QPushButton("Save As...", this);
91 QPushButton *doneButton = new QPushButton("Done", this);
92 QPushButton *cancelButton = new QPushButton("Cancel", this);
93 fileButtons->addWidget(openButton);
94 fileButtons->addWidget(saveButton);
95 fileButtons->addWidget(saveAsButton);
96 fileButtons->addStretch();
97 fileButtons->addWidget(doneButton);
98 fileButtons->addWidget(cancelButton);
99 mainLayout->addLayout(fileButtons);
100
101 connect(addButton, &QPushButton::clicked, this, &AudioPlaylistDialog::addTracks);
102 connect(removeButton, &QPushButton::clicked, this, &AudioPlaylistDialog::removeSelected);
103 connect(upButton, &QPushButton::clicked, this, &AudioPlaylistDialog::moveUp);
104 connect(downButton, &QPushButton::clicked, this, &AudioPlaylistDialog::moveDown);
105 connect(sortButton, &QPushButton::clicked, this, &AudioPlaylistDialog::sortTracks);
106 connect(shuffleButton, &QPushButton::clicked, this, &AudioPlaylistDialog::shuffleTracks);
107 connect(clearButton, &QPushButton::clicked, this, &AudioPlaylistDialog::clearTracks);
108 connect(openButton, &QPushButton::clicked, this, &AudioPlaylistDialog::openPlaylist);
109 connect(saveButton, &QPushButton::clicked, this, &AudioPlaylistDialog::savePlaylist);
110 connect(saveAsButton, &QPushButton::clicked, this, &AudioPlaylistDialog::savePlaylistAs);
111 connect(doneButton, &QPushButton::clicked, this, &AudioPlaylistDialog::finishEditing);
112 connect(cancelButton, &QPushButton::clicked, this, &AudioPlaylistDialog::reject);
113 connect(trackList, &QListWidget::itemSelectionChanged, this, &AudioPlaylistDialog::updateWindowState);
114 connect(trackList->model(), &QAbstractItemModel::rowsMoved, this, [this]() { setDirty(); });
115
117}
118
120 QSettings settings("LostSideDead", "acmx2");
121 const QString startDirectory = settings.value("audio/playlist_track_dir", QString()).toString();
122 const QStringList paths = QFileDialog::getOpenFileNames(this,
123 "Add Audio Tracks",
124 startDirectory,
125 "Audio Files (*.wav *.mp3 *.flac *.aac *.ogg *.m4a *.wma *.opus "
126 "*.mp4 *.mkv *.mov *.avi);;All Files (*)");
127 if (paths.isEmpty())
128 return;
129
130 settings.setValue("audio/playlist_track_dir", QFileInfo(paths.front()).absolutePath());
131 for (const QString &path : paths) {
132 const QString absolutePath = QFileInfo(path).absoluteFilePath();
133 auto *item = new QListWidgetItem(displayName(absolutePath), trackList);
134 item->setData(TRACK_PATH_ROLE, absolutePath);
135 item->setToolTip(absolutePath);
136 }
137 setDirty();
138}
139
141 QList<int> rows;
142 for (QListWidgetItem *item : trackList->selectedItems())
143 rows.push_back(trackList->row(item));
144 std::sort(rows.begin(), rows.end(), std::greater<int>());
145 for (int row : rows)
146 delete trackList->takeItem(row);
147 if (!rows.isEmpty())
148 setDirty();
149}
150
152 QList<int> rows;
153 for (QListWidgetItem *item : trackList->selectedItems())
154 rows.push_back(trackList->row(item));
155 std::sort(rows.begin(), rows.end());
156 if (rows.isEmpty() || rows.front() == 0)
157 return;
158 for (int row : rows) {
159 QListWidgetItem *item = trackList->takeItem(row);
160 trackList->insertItem(row - 1, item);
161 item->setSelected(true);
162 }
163 trackList->setCurrentRow(rows.front() - 1);
164 setDirty();
165}
166
168 QList<int> rows;
169 for (QListWidgetItem *item : trackList->selectedItems())
170 rows.push_back(trackList->row(item));
171 std::sort(rows.begin(), rows.end(), std::greater<int>());
172 if (rows.isEmpty() || rows.front() == trackList->count() - 1)
173 return;
174 for (int row : rows) {
175 QListWidgetItem *item = trackList->takeItem(row);
176 trackList->insertItem(row + 1, item);
177 item->setSelected(true);
178 }
179 trackList->setCurrentRow(rows.back() + 1);
180 setDirty();
181}
182
184 std::vector<QString> paths;
185 paths.reserve(static_cast<std::size_t>(trackList->count()));
186 for (int row = 0; row < trackList->count(); ++row)
187 paths.push_back(itemPath(row));
188 std::stable_sort(paths.begin(), paths.end(), [](const QString &left, const QString &right) { return QString::compare(displayName(left), displayName(right), Qt::CaseInsensitive) < 0; });
189
190 trackList->clear();
191 for (const QString &path : paths) {
192 auto *item = new QListWidgetItem(displayName(path), trackList);
193 item->setData(TRACK_PATH_ROLE, path);
194 item->setToolTip(path);
195 }
196 if (!paths.empty())
197 setDirty();
198}
199
201 std::vector<QString> paths;
202 paths.reserve(static_cast<std::size_t>(trackList->count()));
203 for (int row = 0; row < trackList->count(); ++row)
204 paths.push_back(itemPath(row));
205 std::mt19937 generator(QRandomGenerator::global()->generate());
206 std::shuffle(paths.begin(), paths.end(), generator);
207
208 trackList->clear();
209 for (const QString &path : paths) {
210 auto *item = new QListWidgetItem(displayName(path), trackList);
211 item->setData(TRACK_PATH_ROLE, path);
212 item->setToolTip(path);
213 }
214 if (paths.size() > 1)
215 setDirty();
216}
217
219 if (trackList->count() == 0)
220 return;
221 if (QMessageBox::question(this, "Clear Playlist", "Remove every track from the playlist?") != QMessageBox::Yes)
222 return;
223 trackList->clear();
224 setDirty();
225}
226
228 QSettings settings("LostSideDead", "acmx2");
229 const QString startDirectory = settings.value("audio/playlist_dir", QString()).toString();
230 const QString path = QFileDialog::getOpenFileName(this, "Open Audio Playlist", startDirectory, "M3U Playlists (*.m3u *.m3u8)");
231 if (!path.isEmpty() && confirmDiscardChanges())
232 loadPlaylist(path);
233}
234
235bool AudioPlaylistDialog::loadPlaylist(const QString &path) {
236 QFile file(path);
237 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
238 QMessageBox::critical(this, "Open Playlist", "Could not open the playlist:\n" + path + "\n\n" + file.errorString());
239 return false;
240 }
241
242 const QDir playlistDirectory = QFileInfo(path).absoluteDir();
243 const QList<QByteArray> lines = file.readAll().split('\n');
244 trackList->clear();
245 for (QByteArray encodedLine : lines) {
246 if (encodedLine.endsWith('\r'))
247 encodedLine.chop(1);
248 if (encodedLine.startsWith("\xef\xbb\xbf"))
249 encodedLine.remove(0, 3);
250 QString entry = QString::fromUtf8(encodedLine).trimmed();
251 if (entry.isEmpty() || entry.startsWith('#'))
252 continue;
253
254 QString resolvedPath = entry;
255 if (!isUrl(entry) && QFileInfo(entry).isRelative())
256 resolvedPath = playlistDirectory.absoluteFilePath(entry);
257 if (!isUrl(resolvedPath))
258 resolvedPath = QDir::cleanPath(resolvedPath);
259 auto *item = new QListWidgetItem(displayName(resolvedPath), trackList);
260 item->setData(TRACK_PATH_ROLE, resolvedPath);
261 item->setToolTip(resolvedPath);
262 }
263
264 currentPlaylistPath = QFileInfo(path).absoluteFilePath();
265 QSettings settings("LostSideDead", "acmx2");
266 settings.setValue("audio/playlist_dir", QFileInfo(currentPlaylistPath).absolutePath());
267 setDirty(false);
268 return true;
269}
270
272 if (currentPlaylistPath.isEmpty()) {
274 return;
275 }
277}
278
280 QSettings settings("LostSideDead", "acmx2");
281 QString suggestedPath = currentPlaylistPath;
282 if (suggestedPath.isEmpty()) {
283 const QString directory = settings.value("audio/playlist_dir", QString()).toString();
284 suggestedPath = QDir(directory).filePath("audio-playlist.m3u");
285 }
286 QString path = QFileDialog::getSaveFileName(this, "Save Audio Playlist", suggestedPath, "M3U Playlists (*.m3u);;UTF-8 M3U Playlists (*.m3u8)");
287 if (path.isEmpty())
288 return;
289 if (!path.endsWith(".m3u", Qt::CaseInsensitive) && !path.endsWith(".m3u8", Qt::CaseInsensitive))
290 path += ".m3u";
291 writePlaylist(path);
292}
293
294bool AudioPlaylistDialog::writePlaylist(const QString &path) {
295 if (trackList->count() == 0) {
296 QMessageBox::information(this, "Save Playlist", "Add at least one audio track before saving.");
297 return false;
298 }
299
300 const QFileInfo playlistInfo(path);
301 const QDir playlistDirectory = playlistInfo.absoluteDir();
302 QByteArray contents("#EXTM3U\n");
303 for (int row = 0; row < trackList->count(); ++row) {
304 QString entry = itemPath(row);
305 if (!isUrl(entry))
306 entry = playlistDirectory.relativeFilePath(entry);
307 contents += entry.toUtf8();
308 contents += '\n';
309 }
310
311 QSaveFile file(path);
312 if (!file.open(QIODevice::WriteOnly | QIODevice::Text) || file.write(contents) != contents.size() || !file.commit()) {
313 QMessageBox::critical(this, "Save Playlist", "Could not save the playlist:\n" + path + "\n\n" + file.errorString());
314 return false;
315 }
316
317 currentPlaylistPath = playlistInfo.absoluteFilePath();
318 QSettings settings("LostSideDead", "acmx2");
319 settings.setValue("audio/playlist_dir", playlistInfo.absolutePath());
320 setDirty(false);
321 return true;
322}
323
325 if (dirty) {
326 const QMessageBox::StandardButton choice = QMessageBox::question(this, "Save Playlist", "Save the playlist before closing?", QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save);
327 if (choice == QMessageBox::Cancel)
328 return;
329 if (choice == QMessageBox::Save) {
330 if (currentPlaylistPath.isEmpty())
332 else
334 if (dirty)
335 return;
336 }
337 }
338 accept();
339}
340
343 QDialog::reject();
344}
345
347 if (!dirty)
348 return true;
349 const QMessageBox::StandardButton choice = QMessageBox::question(this, "Unsaved Playlist", "Discard the unsaved playlist changes?", QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel);
350 return choice == QMessageBox::Discard;
351}
352
354 const bool hasTracks = trackList->count() > 0;
355 const bool hasSelection = !trackList->selectedItems().isEmpty();
356 pathLabel->setText(currentPlaylistPath.isEmpty() ? "Playlist: New playlist" : "Playlist: " + currentPlaylistPath);
357 removeButton->setEnabled(hasSelection);
358 upButton->setEnabled(hasSelection);
359 downButton->setEnabled(hasSelection);
360 sortButton->setEnabled(hasTracks);
361 shuffleButton->setEnabled(trackList->count() > 1);
362 clearButton->setEnabled(hasTracks);
363 saveButton->setEnabled(hasTracks);
364 setWindowModified(dirty);
365 setWindowTitle("Audio M3U Playlist Editor[*]");
366}
367
368QString AudioPlaylistDialog::itemPath(int row) const {
369 QListWidgetItem *item = trackList->item(row);
370 return item ? item->data(TRACK_PATH_ROLE).toString() : QString();
371}
372
374 dirty = value;
376}
QPushButton * downButton
QPushButton * removeButton
QPushButton * sortButton
void reject() override
bool writePlaylist(const QString &path)
QString playlistPath() const
Return the playlist most recently opened or saved.
bool loadPlaylist(const QString &path)
QPushButton * clearButton
QPushButton * shuffleButton
QListWidget * trackList
QString itemPath(int row) const
QPushButton * saveButton
void setDirty(bool dirty=true)
AudioPlaylistDialog(const QString &playlistPath=QString(), QWidget *parent=nullptr)
void applyCustomStyleIfEnabled(QWidget *widget)
QString displayName(const QString &path)