add user custom playlist support
This commit is contained in:
+14
-4
@@ -4,7 +4,7 @@ project(SubWave LANGUAGES CXX)
|
|||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia OpenGL OpenGLWidgets)
|
find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia OpenGL OpenGLWidgets Core)
|
||||||
|
|
||||||
# require ffmpeg
|
# require ffmpeg
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
@@ -35,7 +35,10 @@ set(SOURCES
|
|||||||
src/GradientDialog.cpp
|
src/GradientDialog.cpp
|
||||||
src/EqualizerWidget.cpp
|
src/EqualizerWidget.cpp
|
||||||
src/TrackDelegate.cpp
|
src/TrackDelegate.cpp
|
||||||
src/PlaylistModel.cpp
|
src/playlists/PlaylistModel.cpp
|
||||||
|
src/playlists/PlaylistManager.cpp
|
||||||
|
src/playlists/PlaylistCreatorDialog.cpp
|
||||||
|
src/playlists/PlaylistBrowserDialog.cpp
|
||||||
src/MainWindow.cpp
|
src/MainWindow.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,7 +53,10 @@ set(HEADERS
|
|||||||
src/GradientDialog.h
|
src/GradientDialog.h
|
||||||
src/EqualizerWidget.h
|
src/EqualizerWidget.h
|
||||||
src/TrackDelegate.h
|
src/TrackDelegate.h
|
||||||
src/PlaylistModel.h
|
src/playlists/PlaylistModel.h
|
||||||
|
src/playlists/PlaylistManager.h
|
||||||
|
src/playlists/PlaylistCreatorDialog.h
|
||||||
|
src/playlists/PlaylistBrowserDialog.h
|
||||||
src/MainWindow.h
|
src/MainWindow.h
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,7 +72,11 @@ if(WIN32)
|
|||||||
set_target_properties(SubWave PROPERTIES WIN32_EXECUTABLE TRUE)
|
set_target_properties(SubWave PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_include_directories(SubWave PRIVATE src src/engine)
|
target_include_directories(SubWave PRIVATE
|
||||||
|
src
|
||||||
|
src/engine
|
||||||
|
src/playlists
|
||||||
|
)
|
||||||
|
|
||||||
target_link_libraries(SubWave PRIVATE
|
target_link_libraries(SubWave PRIVATE
|
||||||
Qt6::Widgets
|
Qt6::Widgets
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
#include "SpectrogramWidget.h"
|
#include "SpectrogramWidget.h"
|
||||||
#include "EqualizerWidget.h"
|
#include "EqualizerWidget.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#include "PlaylistCreatorDialog.h"
|
||||||
|
#include "PlaylistBrowserDialog.h"
|
||||||
|
#include "PlaylistManager.h"
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
@@ -187,6 +190,15 @@ void MainWindow::buildMenuBar()
|
|||||||
quitAct->setShortcut(QKeySequence::Quit);
|
quitAct->setShortcut(QKeySequence::Quit);
|
||||||
connect(quitAct, &QAction::triggered, qApp, &QApplication::quit);
|
connect(quitAct, &QAction::triggered, qApp, &QApplication::quit);
|
||||||
|
|
||||||
|
// Playlists menu
|
||||||
|
auto *playlists = mb->addMenu("&Playlists");
|
||||||
|
|
||||||
|
auto *newPlaylistAct = playlists->addAction("Create Playlist…");
|
||||||
|
connect(newPlaylistAct, &QAction::triggered, this, &MainWindow::onCreatePlaylist);
|
||||||
|
|
||||||
|
auto *loadPlaylistAct = playlists->addAction("Load Playlist…");
|
||||||
|
connect(loadPlaylistAct, &QAction::triggered, this, &MainWindow::onLoadPlaylist);
|
||||||
|
|
||||||
auto *help = mb->addMenu("&Help");
|
auto *help = mb->addMenu("&Help");
|
||||||
auto *about = help->addAction("About SubWave");
|
auto *about = help->addAction("About SubWave");
|
||||||
connect(about, &QAction::triggered, this, [this]{
|
connect(about, &QAction::triggered, this, [this]{
|
||||||
@@ -359,3 +371,50 @@ void MainWindow::onUiTimer()
|
|||||||
if (m_engine->state() == AudioEngine::State::Stopped)
|
if (m_engine->state() == AudioEngine::State::Stopped)
|
||||||
m_btnPlay->setText("Play");
|
m_btnPlay->setText("Play");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Playlist Management ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void MainWindow::onCreatePlaylist()
|
||||||
|
{
|
||||||
|
PlaylistCreatorDialog dialog(this);
|
||||||
|
if (dialog.exec() == QDialog::Accepted) {
|
||||||
|
// Playlist was created and saved
|
||||||
|
// Optionally auto-load it
|
||||||
|
auto playlist = dialog.getPlaylist();
|
||||||
|
loadPlaylistIntoModel(playlist.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::onLoadPlaylist()
|
||||||
|
{
|
||||||
|
PlaylistBrowserDialog dialog(this);
|
||||||
|
if (dialog.exec() == QDialog::Accepted) {
|
||||||
|
QString playlistName = dialog.getSelectedPlaylist();
|
||||||
|
if (!playlistName.isEmpty()) {
|
||||||
|
loadPlaylistIntoModel(playlistName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::loadPlaylistIntoModel(const QString &playlistName)
|
||||||
|
{
|
||||||
|
auto playlist = PlaylistManager::loadPlaylist(playlistName);
|
||||||
|
|
||||||
|
if (playlist.tracks.isEmpty()) {
|
||||||
|
QMessageBox::warning(this, "Empty Playlist",
|
||||||
|
QString("Playlist \"%1\" is empty.").arg(playlistName));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_model->setTracks(playlist.tracks);
|
||||||
|
m_lblStatus->setText(
|
||||||
|
QString("Playlist: %1 (%2 tracks)")
|
||||||
|
.arg(playlistName)
|
||||||
|
.arg(playlist.tracks.size())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset playback state
|
||||||
|
m_currentIndex = -1;
|
||||||
|
m_engine->stop();
|
||||||
|
m_btnPlay->setText("Play");
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ class PlaylistModel;
|
|||||||
class CoverArtWidget;
|
class CoverArtWidget;
|
||||||
class SpectrogramWidget;
|
class SpectrogramWidget;
|
||||||
class EqualizerWidget;
|
class EqualizerWidget;
|
||||||
|
class PlaylistCreatorDialog;
|
||||||
|
class PlaylistBrowserDialog;
|
||||||
|
|
||||||
class MainWindow : public QMainWindow
|
class MainWindow : public QMainWindow
|
||||||
{
|
{
|
||||||
@@ -43,6 +45,9 @@ private:
|
|||||||
void playNext();
|
void playNext();
|
||||||
void playPrev();
|
void playPrev();
|
||||||
void onUiTimer();
|
void onUiTimer();
|
||||||
|
void onCreatePlaylist();
|
||||||
|
void onLoadPlaylist();
|
||||||
|
void loadPlaylistIntoModel(const QString &playlistName);
|
||||||
|
|
||||||
// Core
|
// Core
|
||||||
AudioEngine *m_engine;
|
AudioEngine *m_engine;
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#include "PlaylistBrowserDialog.h"
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
|
PlaylistBrowserDialog::PlaylistBrowserDialog(QWidget *parent)
|
||||||
|
: QDialog(parent)
|
||||||
|
{
|
||||||
|
setWindowTitle("Load Playlist");
|
||||||
|
setMinimumWidth(400);
|
||||||
|
setMinimumHeight(350);
|
||||||
|
buildUI();
|
||||||
|
refreshPlaylistList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistBrowserDialog::buildUI()
|
||||||
|
{
|
||||||
|
auto *vlay = new QVBoxLayout(this);
|
||||||
|
|
||||||
|
vlay->addWidget(new QLabel("Available Playlists:"));
|
||||||
|
|
||||||
|
m_playlistList = new QListWidget;
|
||||||
|
m_playlistList->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
|
vlay->addWidget(m_playlistList);
|
||||||
|
|
||||||
|
connect(m_playlistList, &QListWidget::itemDoubleClicked,
|
||||||
|
this, &PlaylistBrowserDialog::onPlaylistDoubleClicked);
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
auto *btnRow = new QHBoxLayout;
|
||||||
|
btnRow->addStretch();
|
||||||
|
|
||||||
|
m_deleteBtn = new QPushButton("Delete");
|
||||||
|
m_loadBtn = new QPushButton("Load");
|
||||||
|
|
||||||
|
btnRow->addWidget(m_deleteBtn);
|
||||||
|
btnRow->addWidget(m_loadBtn);
|
||||||
|
|
||||||
|
auto *closeBtn = new QPushButton("Close");
|
||||||
|
btnRow->addWidget(closeBtn);
|
||||||
|
|
||||||
|
vlay->addLayout(btnRow);
|
||||||
|
|
||||||
|
connect(m_deleteBtn, &QPushButton::clicked, this, &PlaylistBrowserDialog::onDeletePlaylist);
|
||||||
|
connect(m_loadBtn, &QPushButton::clicked, this, &PlaylistBrowserDialog::onLoadPlaylist);
|
||||||
|
connect(closeBtn, &QPushButton::clicked, this, &QDialog::reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistBrowserDialog::onPlaylistDoubleClicked(QListWidgetItem *item)
|
||||||
|
{
|
||||||
|
if (item) {
|
||||||
|
m_selectedPlaylist = item->text();
|
||||||
|
accept();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistBrowserDialog::onLoadPlaylist()
|
||||||
|
{
|
||||||
|
QListWidgetItem *current = m_playlistList->currentItem();
|
||||||
|
if (!current) {
|
||||||
|
QMessageBox::warning(this, "No Selection", "Please select a playlist to load.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_selectedPlaylist = current->text();
|
||||||
|
accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistBrowserDialog::onDeletePlaylist()
|
||||||
|
{
|
||||||
|
QListWidgetItem *current = m_playlistList->currentItem();
|
||||||
|
if (!current) {
|
||||||
|
QMessageBox::warning(this, "No Selection", "Please select a playlist to delete.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString playlistName = current->text();
|
||||||
|
|
||||||
|
int ret = QMessageBox::warning(this, "Confirm Delete",
|
||||||
|
QString("Are you sure you want to delete the playlist \"%1\"?").arg(playlistName),
|
||||||
|
QMessageBox::Yes | QMessageBox::No);
|
||||||
|
|
||||||
|
if (ret == QMessageBox::Yes) {
|
||||||
|
if (PlaylistManager::deletePlaylist(playlistName)) {
|
||||||
|
QMessageBox::information(this, "Success",
|
||||||
|
QString("Playlist \"%1\" deleted.").arg(playlistName));
|
||||||
|
refreshPlaylistList();
|
||||||
|
} else {
|
||||||
|
QMessageBox::critical(this, "Error", "Failed to delete playlist.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistBrowserDialog::refreshPlaylistList()
|
||||||
|
{
|
||||||
|
m_playlistList->clear();
|
||||||
|
|
||||||
|
QStringList playlists = PlaylistManager::listPlaylists();
|
||||||
|
for (const QString &name : playlists) {
|
||||||
|
m_playlistList->addItem(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playlists.isEmpty()) {
|
||||||
|
m_playlistList->addItem("(No playlists created)");
|
||||||
|
m_deleteBtn->setEnabled(false);
|
||||||
|
m_loadBtn->setEnabled(false);
|
||||||
|
} else {
|
||||||
|
m_deleteBtn->setEnabled(true);
|
||||||
|
m_loadBtn->setEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QListWidget>
|
||||||
|
#include "PlaylistManager.h"
|
||||||
|
|
||||||
|
class PlaylistBrowserDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit PlaylistBrowserDialog(QWidget *parent = nullptr);
|
||||||
|
QString getSelectedPlaylist() const { return m_selectedPlaylist; }
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onPlaylistDoubleClicked(QListWidgetItem *item);
|
||||||
|
void onDeletePlaylist();
|
||||||
|
void onLoadPlaylist();
|
||||||
|
void refreshPlaylistList();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void buildUI();
|
||||||
|
|
||||||
|
QListWidget *m_playlistList;
|
||||||
|
QPushButton *m_deleteBtn;
|
||||||
|
QPushButton *m_loadBtn;
|
||||||
|
QString m_selectedPlaylist;
|
||||||
|
};
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
#include "PlaylistCreatorDialog.h"
|
||||||
|
#include "MetadataReader.h"
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
PlaylistCreatorDialog::PlaylistCreatorDialog(QWidget *parent)
|
||||||
|
: QDialog(parent)
|
||||||
|
{
|
||||||
|
setWindowTitle("Create Playlist");
|
||||||
|
setMinimumWidth(500);
|
||||||
|
setMinimumHeight(400);
|
||||||
|
buildUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistCreatorDialog::buildUI()
|
||||||
|
{
|
||||||
|
auto *vlay = new QVBoxLayout(this);
|
||||||
|
|
||||||
|
// Name input
|
||||||
|
auto *nameRow = new QHBoxLayout;
|
||||||
|
nameRow->addWidget(new QLabel("Playlist Name:"));
|
||||||
|
m_nameEdit = new QLineEdit;
|
||||||
|
m_nameEdit->setPlaceholderText("e.g., My Favorite Songs");
|
||||||
|
nameRow->addWidget(m_nameEdit);
|
||||||
|
vlay->addLayout(nameRow);
|
||||||
|
|
||||||
|
// Tracks list
|
||||||
|
vlay->addWidget(new QLabel("Tracks:"));
|
||||||
|
m_trackList = new QListWidget;
|
||||||
|
m_trackList->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
|
vlay->addWidget(m_trackList);
|
||||||
|
|
||||||
|
// Buttons for track management
|
||||||
|
auto *trackBtnRow = new QHBoxLayout;
|
||||||
|
m_addBtn = new QPushButton("Add Files...");
|
||||||
|
m_removeBtn = new QPushButton("Remove Selected");
|
||||||
|
trackBtnRow->addWidget(m_addBtn);
|
||||||
|
trackBtnRow->addWidget(m_removeBtn);
|
||||||
|
trackBtnRow->addStretch();
|
||||||
|
vlay->addLayout(trackBtnRow);
|
||||||
|
|
||||||
|
connect(m_addBtn, &QPushButton::clicked, this, &PlaylistCreatorDialog::onAddFiles);
|
||||||
|
connect(m_removeBtn, &QPushButton::clicked, this, &PlaylistCreatorDialog::onRemoveSelected);
|
||||||
|
|
||||||
|
// Dialog buttons
|
||||||
|
vlay->addSpacing(10);
|
||||||
|
auto *dialogBtnRow = new QHBoxLayout;
|
||||||
|
dialogBtnRow->addStretch();
|
||||||
|
|
||||||
|
m_createBtn = new QPushButton("Create Playlist");
|
||||||
|
auto *cancelBtn = new QPushButton("Cancel");
|
||||||
|
|
||||||
|
dialogBtnRow->addWidget(m_createBtn);
|
||||||
|
dialogBtnRow->addWidget(cancelBtn);
|
||||||
|
|
||||||
|
vlay->addLayout(dialogBtnRow);
|
||||||
|
|
||||||
|
connect(m_createBtn, &QPushButton::clicked, this, &PlaylistCreatorDialog::onCreatePlaylist);
|
||||||
|
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistCreatorDialog::onAddFiles()
|
||||||
|
{
|
||||||
|
QStringList files = QFileDialog::getOpenFileNames(
|
||||||
|
this,
|
||||||
|
"Select audio files",
|
||||||
|
QDir::homePath(),
|
||||||
|
"Audio Files (*.mp3 *.flac *.wav *.ogg *.aac *.m4a *.opus *.wma *.ape *.aiff *.alac);;All Files (*)"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const QString &filePath : files) {
|
||||||
|
if (filePath.isEmpty()) continue;
|
||||||
|
|
||||||
|
// Check if already in playlist
|
||||||
|
bool alreadyAdded = false;
|
||||||
|
for (const Track &t : m_playlist.tracks) {
|
||||||
|
if (t.filePath == filePath) {
|
||||||
|
alreadyAdded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alreadyAdded) {
|
||||||
|
Track track;
|
||||||
|
track.filePath = filePath;
|
||||||
|
|
||||||
|
// Set default metadata
|
||||||
|
QString ext = QFileInfo(filePath).suffix().toLower();
|
||||||
|
track.format = ext.toUpper();
|
||||||
|
track.title = QFileInfo(filePath).completeBaseName();
|
||||||
|
track.artist = "Unknown Artist";
|
||||||
|
track.album = "Unknown Album";
|
||||||
|
|
||||||
|
// Extract actual metadata and cover art
|
||||||
|
MetadataReader::populate(track);
|
||||||
|
|
||||||
|
m_playlist.tracks.append(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTrackList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistCreatorDialog::onRemoveSelected()
|
||||||
|
{
|
||||||
|
int row = m_trackList->currentRow();
|
||||||
|
if (row >= 0 && row < m_playlist.tracks.size()) {
|
||||||
|
m_playlist.tracks.removeAt(row);
|
||||||
|
updateTrackList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistCreatorDialog::onCreatePlaylist()
|
||||||
|
{
|
||||||
|
QString name = m_nameEdit->text().trimmed();
|
||||||
|
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
QMessageBox::warning(this, "Invalid Name", "Please enter a playlist name.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_playlist.tracks.isEmpty()) {
|
||||||
|
QMessageBox::warning(this, "No Tracks", "Please add at least one track to the playlist.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_playlist.name = name;
|
||||||
|
|
||||||
|
if (PlaylistManager::savePlaylist(m_playlist)) {
|
||||||
|
QMessageBox::information(this, "Success",
|
||||||
|
QString("Playlist \"%1\" created with %2 tracks.")
|
||||||
|
.arg(name)
|
||||||
|
.arg(m_playlist.tracks.size()));
|
||||||
|
accept();
|
||||||
|
} else {
|
||||||
|
QMessageBox::critical(this, "Error", "Failed to save playlist.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistCreatorDialog::updateTrackList()
|
||||||
|
{
|
||||||
|
m_trackList->clear();
|
||||||
|
|
||||||
|
for (const Track &track : m_playlist.tracks) {
|
||||||
|
QString display = track.displayTitle();
|
||||||
|
if (!track.artist.isEmpty()) {
|
||||||
|
display = track.artist + " - " + display;
|
||||||
|
}
|
||||||
|
m_trackList->addItem(display);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QListWidget>
|
||||||
|
#include "PlaylistManager.h"
|
||||||
|
|
||||||
|
class PlaylistCreatorDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit PlaylistCreatorDialog(QWidget *parent = nullptr);
|
||||||
|
PlaylistManager::Playlist getPlaylist() const { return m_playlist; }
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onAddFiles();
|
||||||
|
void onRemoveSelected();
|
||||||
|
void onCreatePlaylist();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void buildUI();
|
||||||
|
void updateTrackList();
|
||||||
|
|
||||||
|
PlaylistManager::Playlist m_playlist;
|
||||||
|
QLineEdit *m_nameEdit;
|
||||||
|
QListWidget *m_trackList;
|
||||||
|
QPushButton *m_addBtn;
|
||||||
|
QPushButton *m_removeBtn;
|
||||||
|
QPushButton *m_createBtn;
|
||||||
|
};
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
#include "PlaylistManager.h"
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include "MetadataReader.h"
|
||||||
|
|
||||||
|
QString PlaylistManager::playlistsDir()
|
||||||
|
{
|
||||||
|
return QDir::homePath() + "/.subwave/playlists";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlaylistManager::ensurePlaylistsDir()
|
||||||
|
{
|
||||||
|
QDir dir;
|
||||||
|
return dir.mkpath(playlistsDir());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PlaylistManager::playlistFilePath(const QString &playlistName)
|
||||||
|
{
|
||||||
|
QString sanitized = sanitizePlaylistName(playlistName);
|
||||||
|
return playlistsDir() + "/" + sanitized + ".json";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PlaylistManager::sanitizePlaylistName(const QString &name)
|
||||||
|
{
|
||||||
|
QString result = name;
|
||||||
|
result.replace('/', "_")
|
||||||
|
.replace('\\', "_")
|
||||||
|
.replace(':', "_")
|
||||||
|
.replace('"', "_")
|
||||||
|
.replace('?', "_")
|
||||||
|
.replace('*', "_")
|
||||||
|
.replace('<', "_")
|
||||||
|
.replace('>', "_")
|
||||||
|
.replace('|', "_");
|
||||||
|
return result.trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaylistManager::Playlist PlaylistManager::loadPlaylist(const QString &playlistName)
|
||||||
|
{
|
||||||
|
Playlist playlist;
|
||||||
|
playlist.name = playlistName;
|
||||||
|
playlist.filePath = playlistFilePath(playlistName);
|
||||||
|
|
||||||
|
QFile file(playlist.filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
return playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString json = QString::fromUtf8(file.readAll());
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
playlist = parsePlaylistJson(json);
|
||||||
|
playlist.name = playlistName;
|
||||||
|
playlist.filePath = playlistFilePath(playlistName);
|
||||||
|
|
||||||
|
return playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlaylistManager::savePlaylist(const Playlist &playlist)
|
||||||
|
{
|
||||||
|
if (!ensurePlaylistsDir()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString filePath = playlistFilePath(playlist.name);
|
||||||
|
QFile file(filePath);
|
||||||
|
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString json = playlistToJson(playlist);
|
||||||
|
file.write(json.toUtf8());
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlaylistManager::deletePlaylist(const QString &playlistName)
|
||||||
|
{
|
||||||
|
QString filePath = playlistFilePath(playlistName);
|
||||||
|
QFile file(filePath);
|
||||||
|
return file.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList PlaylistManager::listPlaylists()
|
||||||
|
{
|
||||||
|
QStringList result;
|
||||||
|
QDir dir(playlistsDir());
|
||||||
|
|
||||||
|
if (!dir.exists()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFileInfoList files = dir.entryInfoList(
|
||||||
|
QStringList() << "*.json",
|
||||||
|
QDir::Files,
|
||||||
|
QDir::Name
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const QFileInfo &info : files) {
|
||||||
|
QString name = info.baseName();
|
||||||
|
result.append(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistManager::addTrackToPlaylist(Playlist &playlist, const Track &track)
|
||||||
|
{
|
||||||
|
playlist.tracks.append(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistManager::removeTrackFromPlaylist(Playlist &playlist, int index)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < playlist.tracks.size()) {
|
||||||
|
playlist.tracks.removeAt(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaylistManager::clearPlaylist(Playlist &playlist)
|
||||||
|
{
|
||||||
|
playlist.tracks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaylistManager::Playlist PlaylistManager::parsePlaylistJson(const QString &json)
|
||||||
|
{
|
||||||
|
Playlist playlist;
|
||||||
|
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
|
||||||
|
if (!doc.isObject()) {
|
||||||
|
return playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject obj = doc.object();
|
||||||
|
if (obj.contains("tracks") && obj["tracks"].isArray()) {
|
||||||
|
QJsonArray tracksArray = obj["tracks"].toArray();
|
||||||
|
|
||||||
|
for (int i = 0; i < tracksArray.size(); ++i) {
|
||||||
|
QJsonObject trackObj = tracksArray[i].toObject();
|
||||||
|
|
||||||
|
Track track;
|
||||||
|
track.filePath = trackObj["filePath"].toString();
|
||||||
|
track.title = trackObj["title"].toString();
|
||||||
|
track.artist = trackObj["artist"].toString();
|
||||||
|
track.album = trackObj["album"].toString();
|
||||||
|
track.year = trackObj["year"].toInt(0);
|
||||||
|
track.trackNumber = trackObj["trackNumber"].toInt(0);
|
||||||
|
track.durationSecs = trackObj["durationSecs"].toInt(0);
|
||||||
|
track.format = trackObj["format"].toString();
|
||||||
|
|
||||||
|
if (!track.filePath.isEmpty()) {
|
||||||
|
// Re-extract metadata and cover art from the file
|
||||||
|
// in case it has changed or cover art wasn't saved
|
||||||
|
MetadataReader::populate(track);
|
||||||
|
playlist.tracks.append(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PlaylistManager::playlistToJson(const Playlist &playlist)
|
||||||
|
{
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonArray tracksArray;
|
||||||
|
|
||||||
|
for (const Track &track : playlist.tracks) {
|
||||||
|
QJsonObject trackObj;
|
||||||
|
trackObj["filePath"] = track.filePath;
|
||||||
|
trackObj["title"] = track.title;
|
||||||
|
trackObj["artist"] = track.artist;
|
||||||
|
trackObj["album"] = track.album;
|
||||||
|
trackObj["year"] = track.year;
|
||||||
|
trackObj["trackNumber"] = track.trackNumber;
|
||||||
|
trackObj["durationSecs"] = static_cast<int>(track.durationSecs);
|
||||||
|
trackObj["format"] = track.format;
|
||||||
|
|
||||||
|
tracksArray.append(trackObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
root["tracks"] = tracksArray;
|
||||||
|
|
||||||
|
QJsonDocument doc(root);
|
||||||
|
return QString::fromUtf8(doc.toJson());
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QList>
|
||||||
|
#include "Track.h"
|
||||||
|
|
||||||
|
class PlaylistManager {
|
||||||
|
public:
|
||||||
|
struct Playlist {
|
||||||
|
QString name;
|
||||||
|
QList<Track> tracks;
|
||||||
|
QString filePath;
|
||||||
|
};
|
||||||
|
|
||||||
|
static QString playlistsDir();
|
||||||
|
static bool ensurePlaylistsDir();
|
||||||
|
|
||||||
|
// Playlist management
|
||||||
|
static Playlist loadPlaylist(const QString &playlistName);
|
||||||
|
static bool savePlaylist(const Playlist &playlist);
|
||||||
|
static bool deletePlaylist(const QString &playlistName);
|
||||||
|
static QStringList listPlaylists();
|
||||||
|
|
||||||
|
// Track management
|
||||||
|
static void addTrackToPlaylist(Playlist &playlist, const Track &track);
|
||||||
|
static void removeTrackFromPlaylist(Playlist &playlist, int index);
|
||||||
|
static void clearPlaylist(Playlist &playlist);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QString playlistFilePath(const QString &playlistName);
|
||||||
|
static QString sanitizePlaylistName(const QString &name);
|
||||||
|
static Playlist parsePlaylistJson(const QString &json);
|
||||||
|
static QString playlistToJson(const Playlist &playlist);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user