2025-02-16 00:50:14 +08:00
|
|
|
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
|
2022-06-19 16:17:38 +08:00
|
|
|
//
|
|
|
|
|
// SPDX-License-Identifier: MIT
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2021-02-09 14:19:09 +08:00
|
|
|
#include "playlistmanager.h"
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2021-02-09 14:19:09 +08:00
|
|
|
#include <QCollator>
|
|
|
|
|
#include <QDir>
|
|
|
|
|
#include <QFileInfo>
|
|
|
|
|
#include <QUrl>
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
PlaylistManager::PlaylistManager(QObject *parent)
|
|
|
|
|
: QObject(parent)
|
|
|
|
|
, m_currentIndex(std::nullopt)
|
|
|
|
|
, m_playlist()
|
|
|
|
|
, m_allowedSuffixes()
|
2021-02-09 14:19:09 +08:00
|
|
|
{
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
PlaylistManager::~PlaylistManager()
|
2021-02-09 14:19:09 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
// region: Allowed Suffixes Operations
|
|
|
|
|
|
|
|
|
|
const QStringList& PlaylistManager::allowedSuffixes() const {
|
|
|
|
|
return m_allowedSuffixes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PlaylistManager::setAllowedSuffixes(const QStringList& nameFilters) {
|
|
|
|
|
m_allowedSuffixes = nameFilters;
|
|
|
|
|
emit allowedSuffixesChanged(m_allowedSuffixes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// endregion
|
|
|
|
|
|
|
|
|
|
// region: Playlist Loader and Reset
|
|
|
|
|
|
|
|
|
|
bool PlaylistManager::loadPlaylist(const QStringList& files) {
|
|
|
|
|
// Convert string list to url list first
|
|
|
|
|
QList<QUrl> urls;
|
|
|
|
|
for (const QString & file : std::as_const(files)) {
|
|
|
|
|
urls.append(urlFromString(file));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return loadPlaylist(urls);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool PlaylistManager::loadPlaylist(const QList<QUrl>& urls) {
|
|
|
|
|
// Check its count, and check M3U8 playlist for single file case.
|
|
|
|
|
if (urls.isEmpty()) {
|
|
|
|
|
clearPlaylist();
|
|
|
|
|
return true;
|
|
|
|
|
} else if (urls.count() == 1) {
|
|
|
|
|
const QUrl& firstUrl = urls.first();
|
|
|
|
|
const QString lowerCaseUrlPath(firstUrl.path().toLower());
|
|
|
|
|
if (lowerCaseUrlPath.endsWith(".m3u8") || lowerCaseUrlPath.endsWith(".m3u")) {
|
|
|
|
|
return loadM3U8Playlist(firstUrl);
|
|
|
|
|
} else {
|
|
|
|
|
return loadSingleFilePlaylist(firstUrl);
|
|
|
|
|
}
|
2024-07-20 23:18:42 +08:00
|
|
|
} else {
|
2026-07-13 15:40:59 +08:00
|
|
|
return loadMultipleFilesPlaylist(std::move(urls));
|
2024-07-20 23:18:42 +08:00
|
|
|
}
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
bool PlaylistManager::loadSingleFilePlaylist(const QUrl& url) {
|
|
|
|
|
// Check URL at first
|
|
|
|
|
if (!checkUrl(url))
|
|
|
|
|
return false;
|
|
|
|
|
|
2024-07-20 23:18:42 +08:00
|
|
|
QFileInfo info(url.toLocalFile());
|
|
|
|
|
QDir dir(info.path());
|
2026-07-13 15:40:59 +08:00
|
|
|
QString currentFileName = info.fileName();
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2024-07-20 23:18:42 +08:00
|
|
|
QStringList entryList = dir.entryList(
|
2026-07-13 15:40:59 +08:00
|
|
|
m_allowedSuffixes, QDir::Files | QDir::NoSymLinks, QDir::NoSort);
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2024-07-20 23:18:42 +08:00
|
|
|
QCollator collator;
|
|
|
|
|
collator.setNumericMode(true);
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2024-07-20 23:18:42 +08:00
|
|
|
std::sort(entryList.begin(), entryList.end(), collator);
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2024-07-20 23:18:42 +08:00
|
|
|
QList<QUrl> playlist;
|
2026-07-13 15:40:59 +08:00
|
|
|
std::optional<qsizetype> idx = std::nullopt;
|
|
|
|
|
for (qsizetype i = 0; i < entryList.count(); i++) {
|
|
|
|
|
const QString &fileName = entryList.at(i);
|
|
|
|
|
const QString &oneEntry = dir.absoluteFilePath(fileName);
|
|
|
|
|
const QUrl &url = QUrl::fromLocalFile(oneEntry);
|
|
|
|
|
if (!checkUrl(url)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2024-07-20 23:18:42 +08:00
|
|
|
playlist.append(url);
|
|
|
|
|
if (fileName == currentFileName) {
|
2024-07-28 16:14:14 +08:00
|
|
|
idx = i;
|
2024-07-20 23:18:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
setPlaylist(std::move(playlist), idx);
|
|
|
|
|
return true;
|
2021-02-26 00:57:12 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
bool PlaylistManager::loadMultipleFilesPlaylist(const QList<QUrl>&& urls) {
|
|
|
|
|
// Check all of them are valid
|
|
|
|
|
for (const auto& url : std::as_const(urls)) {
|
|
|
|
|
if (!checkUrl(url)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
// Set playlist and return
|
|
|
|
|
setPlaylist(std::move(urls));
|
2024-07-20 23:18:42 +08:00
|
|
|
return true;
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
bool PlaylistManager::loadM3U8Playlist(const QUrl& url) {
|
|
|
|
|
// Check URL at first
|
|
|
|
|
if (!checkUrl(url))
|
|
|
|
|
return false;
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
QList<QUrl> urls;
|
|
|
|
|
QFile file(url.toLocalFile());
|
|
|
|
|
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
|
|
|
while (!file.atEnd()) {
|
|
|
|
|
QString line = file.readLine();
|
|
|
|
|
if (line.startsWith('#')) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
QFileInfo fileInfo(file);
|
|
|
|
|
QUrl item = QUrl::fromUserInput(line, fileInfo.absolutePath());
|
|
|
|
|
if (!checkUrl(item)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
urls.append(item);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
setPlaylist(std::move(urls));
|
|
|
|
|
return true;
|
2024-07-20 23:18:42 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
void PlaylistManager::setPlaylist(const QList<QUrl>&& urls, std::optional<qsizetype> indexHint) {
|
|
|
|
|
// Setup playlist
|
|
|
|
|
m_playlist = std::move(urls);
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
// Set index with hint
|
|
|
|
|
if (indexHint.has_value()) {
|
|
|
|
|
m_currentIndex = *indexHint;
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2026-07-13 15:40:59 +08:00
|
|
|
sanitizeIndex();
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
// Raise signal
|
|
|
|
|
emit playlistChanged(totalCount());
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
void PlaylistManager::clearPlaylist() {
|
|
|
|
|
m_playlist.clear();
|
|
|
|
|
sanitizeIndex();
|
|
|
|
|
emit playlistChanged(this->totalCount());
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
// endregion
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
// region: Playlist Operations
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
void PlaylistManager::appendToPlaylist(const QString &file)
|
2024-04-15 01:16:00 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
return appendToPlaylist(urlFromString(file));
|
2024-07-20 23:18:42 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
void PlaylistManager::appendToPlaylist(const QUrl &url)
|
2024-07-20 23:18:42 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
m_playlist.append(url);
|
|
|
|
|
sanitizeIndex();
|
|
|
|
|
emit playlistChanged(totalCount());
|
2024-04-15 01:16:00 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
bool PlaylistManager::insertIntoPlaylist(qsizetype index, const QString &file)
|
2021-02-09 14:19:09 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
return insertIntoPlaylist(index, urlFromString(file));
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
bool PlaylistManager::insertIntoPlaylist(qsizetype index, const QUrl &url)
|
2024-07-21 00:52:32 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
if (index < 0 || index > totalCount()) {
|
|
|
|
|
return false;
|
|
|
|
|
} else {
|
|
|
|
|
m_playlist.insert(index, url);
|
|
|
|
|
sanitizeIndex();
|
|
|
|
|
emit playlistChanged(totalCount());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2024-07-21 00:52:32 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
bool PlaylistManager::removeFromPlaylist(qsizetype index)
|
2025-06-28 09:27:30 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
if (index < 0 || index >= totalCount()) {
|
|
|
|
|
return false;
|
2025-06-28 09:27:30 +08:00
|
|
|
} else {
|
2026-07-13 15:40:59 +08:00
|
|
|
m_playlist.removeAt(index);
|
|
|
|
|
sanitizeIndex();
|
|
|
|
|
emit playlistChanged(totalCount());
|
|
|
|
|
return true;
|
2025-06-28 09:27:30 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
// endregion
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
// region: Index Operations
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
qsizetype PlaylistManager::totalCount() const
|
|
|
|
|
{
|
|
|
|
|
return m_playlist.count();
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
std::optional<qsizetype> PlaylistManager::previousIndex() const
|
2021-02-09 14:19:09 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
if (!m_currentIndex.has_value() || totalCount() <= 0) return std::nullopt;
|
|
|
|
|
return (isFirstIndex() ? totalCount() - 1 : *m_currentIndex - 1);
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
std::optional<qsizetype> PlaylistManager::nextIndex() const
|
2021-02-09 14:19:09 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
if (!m_currentIndex.has_value() || totalCount() <= 0) return std::nullopt;
|
|
|
|
|
return (isLastIndex() ? 0 : *m_currentIndex + 1);
|
2024-07-20 23:18:42 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2025-06-25 20:55:42 +08:00
|
|
|
bool PlaylistManager::isFirstIndex() const
|
|
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
return m_currentIndex.has_value() && *m_currentIndex == 0;
|
2025-06-25 20:55:42 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2025-06-25 20:55:42 +08:00
|
|
|
bool PlaylistManager::isLastIndex() const
|
|
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
return m_currentIndex.has_value() && *m_currentIndex + 1 == totalCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::optional<qsizetype> PlaylistManager::currentIndex() const
|
|
|
|
|
{
|
|
|
|
|
return m_currentIndex;
|
2025-06-25 20:55:42 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
bool PlaylistManager::setCurrentIndex(qsizetype index)
|
2024-07-20 23:18:42 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
if (!m_currentIndex.has_value()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (index < 0 || index >= totalCount()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (*m_currentIndex == index) {
|
|
|
|
|
return true;
|
2024-07-20 23:18:42 +08:00
|
|
|
}
|
2026-07-13 15:40:59 +08:00
|
|
|
|
|
|
|
|
m_currentIndex = index;
|
|
|
|
|
emit currentIndexChanged(index);
|
|
|
|
|
return true;
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
QUrl PlaylistManager::urlByIndex(qsizetype index) const
|
2021-02-09 14:19:09 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
return m_playlist.value(index);
|
2024-07-20 23:18:42 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
QString PlaylistManager::localFileByIndex(qsizetype index) const
|
2024-07-20 23:18:42 +08:00
|
|
|
{
|
|
|
|
|
return urlByIndex(index).toLocalFile();
|
|
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
// endregion
|
|
|
|
|
|
|
|
|
|
// region: Misc
|
|
|
|
|
|
|
|
|
|
void PlaylistManager::sanitizeIndex()
|
2024-07-20 23:18:42 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
auto cnt = totalCount();
|
|
|
|
|
if (cnt == 0) {
|
|
|
|
|
// If playlist is empty, current index should be invalid.
|
|
|
|
|
m_currentIndex = std::nullopt;
|
|
|
|
|
} else {
|
|
|
|
|
if (m_currentIndex.has_value()) {
|
|
|
|
|
// Clamp current index in playlist range.
|
|
|
|
|
// If it is already in range, not change was done.
|
|
|
|
|
auto currentIndex = *m_currentIndex;
|
|
|
|
|
if (currentIndex < 0) {
|
|
|
|
|
m_currentIndex = 0;
|
|
|
|
|
} else if (currentIndex >= cnt) {
|
|
|
|
|
m_currentIndex = cnt - 1;
|
|
|
|
|
} else {
|
|
|
|
|
; // No change.
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// If there is no set value for current index,
|
|
|
|
|
// set it at the beginning of playlist.
|
|
|
|
|
m_currentIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-20 23:18:42 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
bool PlaylistManager::checkUrl(const QUrl& url)
|
2024-07-20 23:18:42 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
return url.isValid() && url.isLocalFile();
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2025-07-23 21:20:34 +08:00
|
|
|
|
2026-07-13 15:40:59 +08:00
|
|
|
QUrl PlaylistManager::urlFromString(const QString& file)
|
2021-02-09 14:19:09 +08:00
|
|
|
{
|
2026-07-13 15:40:59 +08:00
|
|
|
return QUrl::fromLocalFile(file);
|
2021-02-09 14:19:09 +08:00
|
|
|
}
|
2026-07-13 15:40:59 +08:00
|
|
|
|
|
|
|
|
// endregion
|