49 setWindowTitle(
"Audio M3U Playlist Editor");
50 setMinimumSize(720, 520);
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.",
56 instructions->setWordWrap(
true);
57 mainLayout->addWidget(instructions);
60 pathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
65 trackList->setSelectionMode(QAbstractItemView::ExtendedSelection);
66 trackList->setDragDropMode(QAbstractItemView::InternalMove);
67 trackList->setDefaultDropAction(Qt::MoveAction);
70 QHBoxLayout *trackButtons =
new QHBoxLayout();
71 QPushButton *addButton =
new QPushButton(
"Add Tracks...",
this);
73 upButton =
new QPushButton(
"Move Up",
this);
74 downButton =
new QPushButton(
"Move Down",
this);
78 trackButtons->addWidget(addButton);
85 mainLayout->addLayout(trackButtons);
87 QHBoxLayout *fileButtons =
new QHBoxLayout();
88 QPushButton *openButton =
new QPushButton(
"Open M3U...",
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);
95 fileButtons->addWidget(saveAsButton);
96 fileButtons->addStretch();
97 fileButtons->addWidget(doneButton);
98 fileButtons->addWidget(cancelButton);
99 mainLayout->addLayout(fileButtons);
114 connect(
trackList->model(), &QAbstractItemModel::rowsMoved,
this, [
this]() { setDirty(); });
120 QSettings settings(
"LostSideDead",
"acmx2");
121 const QString startDirectory = settings.value(
"audio/playlist_track_dir", QString()).toString();
122 const QStringList paths = QFileDialog::getOpenFileNames(
this,
125 "Audio Files (*.wav *.mp3 *.flac *.aac *.ogg *.m4a *.wma *.opus "
126 "*.mp4 *.mkv *.mov *.avi);;All Files (*)");
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);
184 std::vector<QString> paths;
185 paths.reserve(
static_cast<std::size_t
>(
trackList->count()));
186 for (
int row = 0; row <
trackList->count(); ++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; });
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);
201 std::vector<QString> paths;
202 paths.reserve(
static_cast<std::size_t
>(
trackList->count()));
203 for (
int row = 0; row <
trackList->count(); ++row)
205 std::mt19937 generator(QRandomGenerator::global()->generate());
206 std::shuffle(paths.begin(), paths.end(), generator);
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);
214 if (paths.size() > 1)
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());
242 const QDir playlistDirectory = QFileInfo(path).absoluteDir();
243 const QList<QByteArray> lines = file.readAll().split(
'\n');
245 for (QByteArray encodedLine : lines) {
246 if (encodedLine.endsWith(
'\r'))
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(
'#'))
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);
265 QSettings settings(
"LostSideDead",
"acmx2");
280 QSettings settings(
"LostSideDead",
"acmx2");
282 if (suggestedPath.isEmpty()) {
283 const QString directory = settings.value(
"audio/playlist_dir", QString()).toString();
284 suggestedPath = QDir(directory).filePath(
"audio-playlist.m3u");
286 QString path = QFileDialog::getSaveFileName(
this,
"Save Audio Playlist", suggestedPath,
"M3U Playlists (*.m3u);;UTF-8 M3U Playlists (*.m3u8)");
289 if (!path.endsWith(
".m3u", Qt::CaseInsensitive) && !path.endsWith(
".m3u8", Qt::CaseInsensitive))
296 QMessageBox::information(
this,
"Save Playlist",
"Add at least one audio track before saving.");
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) {
306 entry = playlistDirectory.relativeFilePath(entry);
307 contents += entry.toUtf8();
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());
318 QSettings settings(
"LostSideDead",
"acmx2");
319 settings.setValue(
"audio/playlist_dir", playlistInfo.absolutePath());