feat: sidecar .chp and .pbf support
This commit is contained in:
parent
9bbfefaea1
commit
f17b722600
@ -7,9 +7,12 @@ Since **I** just need a simple player which *just works* right now, so I did man
|
|||||||
We have the following features:
|
We have the following features:
|
||||||
|
|
||||||
- [Sidecar](https://en.wikipedia.org/wiki/Sidecar_file) lyrics file (`.lrc`) support with an optional desktop lyrics bar widget
|
- [Sidecar](https://en.wikipedia.org/wiki/Sidecar_file) lyrics file (`.lrc`) support with an optional desktop lyrics bar widget
|
||||||
|
- Sidecar chapter file support
|
||||||
|
- [YouTube-style chapter](https://support.google.com/youtube/answer/9884579) saved to a plain text file with `.chp` suffix
|
||||||
|
- PotPlayer `.pbf` file, `[Bookmark]`s as chapters
|
||||||
- Auto-load all audio files in the same folder of the file that you attempted to play, into a playlist
|
- Auto-load all audio files in the same folder of the file that you attempted to play, into a playlist
|
||||||
|
|
||||||
But these features are not available, some of them are TBD and others are not planned:
|
These features are not available, some of them are TBD and others are not planned:
|
||||||
|
|
||||||
- File format support will be limited by the [FFmpeg version that Qt 6 uses](https://doc.qt.io/qt-6/qtmultimedia-attribution-ffmpeg.html).
|
- File format support will be limited by the [FFmpeg version that Qt 6 uses](https://doc.qt.io/qt-6/qtmultimedia-attribution-ffmpeg.html).
|
||||||
- ...which if you use Qt's official binary, only contains the LGPLv2.1+ part. (already good enough, tho)
|
- ...which if you use Qt's official binary, only contains the LGPLv2.1+ part. (already good enough, tho)
|
||||||
|
@ -242,6 +242,12 @@ void MainWindow::dropEvent(QDropEvent *e)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileName.endsWith(".chp") || fileName.endsWith(".pbf")) {
|
||||||
|
QList<std::pair<qint64, QString>> chapters(PlaybackProgressIndicator::tryLoadSidecarChapterFile(fileName));
|
||||||
|
ui->playbackProgressIndicator->setChapters(chapters);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urls);
|
const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urls);
|
||||||
if (modelIndex.isValid()) {
|
if (modelIndex.isValid()) {
|
||||||
loadByModelIndex(modelIndex);
|
loadByModelIndex(modelIndex);
|
||||||
@ -264,14 +270,21 @@ void MainWindow::loadFile()
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_playlistManager->loadPlaylist(urlList);
|
m_playlistManager->loadPlaylist(urlList);
|
||||||
m_mediaPlayer->setSource(urlList.first());
|
const QUrl & firstUrl = urlList.first();
|
||||||
m_lrcbar->loadLyrics(urlList.first().toLocalFile());
|
const QString firstFilePath = firstUrl.toLocalFile();
|
||||||
|
m_mediaPlayer->setSource(firstUrl);
|
||||||
|
m_lrcbar->loadLyrics(firstFilePath);
|
||||||
|
QList<std::pair<qint64, QString>> chapters(PlaybackProgressIndicator::tryLoadSidecarChapterFile(firstFilePath));
|
||||||
|
ui->playbackProgressIndicator->setChapters(chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::loadByModelIndex(const QModelIndex & index)
|
void MainWindow::loadByModelIndex(const QModelIndex & index)
|
||||||
{
|
{
|
||||||
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
|
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
|
||||||
m_lrcbar->loadLyrics(m_playlistManager->localFileByIndex(index));
|
QString filePath(m_playlistManager->localFileByIndex(index));
|
||||||
|
m_lrcbar->loadLyrics(filePath);
|
||||||
|
QList<std::pair<qint64, QString>> chapters(PlaybackProgressIndicator::tryLoadSidecarChapterFile(filePath));
|
||||||
|
ui->playbackProgressIndicator->setChapters(chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::play()
|
void MainWindow::play()
|
||||||
|
@ -4,8 +4,12 @@
|
|||||||
|
|
||||||
#include "playbackprogressindicator.h"
|
#include "playbackprogressindicator.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
PlaybackProgressIndicator::PlaybackProgressIndicator(QWidget *parent) :
|
PlaybackProgressIndicator::PlaybackProgressIndicator(QWidget *parent) :
|
||||||
QWidget(parent)
|
QWidget(parent)
|
||||||
@ -35,6 +39,91 @@ void PlaybackProgressIndicator::setChapters(QList<std::pair<qint64, QString> > c
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<std::pair<qint64, QString> > PlaybackProgressIndicator::tryLoadSidecarChapterFile(const QString &filePath)
|
||||||
|
{
|
||||||
|
if (filePath.endsWith(".chp", Qt::CaseInsensitive)) {
|
||||||
|
return parseCHPChapterFile(filePath);
|
||||||
|
} else if (filePath.endsWith(".pbf", Qt::CaseInsensitive)) {
|
||||||
|
return parsePBFChapterFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".chp"));
|
||||||
|
if (fileInfo.exists()) {
|
||||||
|
return parseCHPChapterFile(fileInfo.absoluteFilePath());
|
||||||
|
}
|
||||||
|
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".pbf"));
|
||||||
|
if (fileInfo.exists()) {
|
||||||
|
return parsePBFChapterFile(fileInfo.absoluteFilePath());
|
||||||
|
}
|
||||||
|
fileInfo.setFile(filePath + ".chp");
|
||||||
|
if (fileInfo.exists()) {
|
||||||
|
return parseCHPChapterFile(fileInfo.absoluteFilePath());
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<std::pair<qint64, QString> > PlaybackProgressIndicator::parseCHPChapterFile(const QString &filePath)
|
||||||
|
{
|
||||||
|
QList<std::pair<qint64, QString>> chapters;
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextStream in(&file);
|
||||||
|
QRegularExpression timeRegex(R"((\d{1,2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,3}))?)");
|
||||||
|
|
||||||
|
while (!in.atEnd()) {
|
||||||
|
QString line = in.readLine().trimmed();
|
||||||
|
QRegularExpressionMatch match = timeRegex.match(line);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
int hours = match.capturedView(3).isEmpty() ? 0 : match.capturedView(1).toInt();
|
||||||
|
int minutes = match.capturedView(3).isEmpty() ? match.capturedView(1).toInt() : match.capturedView(2).toInt();
|
||||||
|
int seconds = match.capturedView(3).isEmpty() ? match.capturedView(2).toInt() : match.capturedView(3).toInt();
|
||||||
|
int milliseconds = 0;
|
||||||
|
|
||||||
|
QStringView millisecondsStr(match.capturedView(4));
|
||||||
|
if (!millisecondsStr.isEmpty()) {
|
||||||
|
milliseconds = millisecondsStr.toInt() * pow(10, 3 - millisecondsStr.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 totalMilliseconds = (hours * 3600 + minutes * 60 + seconds) * 1000 + milliseconds;
|
||||||
|
|
||||||
|
QString chapterTitle = line.mid(match.capturedLength()).trimmed();
|
||||||
|
chapters.append(std::make_pair(totalMilliseconds, chapterTitle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<std::pair<qint64, QString> > PlaybackProgressIndicator::parsePBFChapterFile(const QString &filePath)
|
||||||
|
{
|
||||||
|
QList<std::pair<qint64, QString>> chapters;
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextStream in(&file);
|
||||||
|
QRegularExpression chapterRegex(R"(^\d+=(\d+)\*([^*]*)\*.*$)");
|
||||||
|
|
||||||
|
while (!in.atEnd()) {
|
||||||
|
QString line = in.readLine().trimmed();
|
||||||
|
QRegularExpressionMatch match = chapterRegex.match(line);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
qint64 timestamp = match.captured(1).toLongLong();
|
||||||
|
QString title = match.captured(2).trimmed();
|
||||||
|
chapters.append(std::make_pair(timestamp, title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
void PlaybackProgressIndicator::paintEvent(QPaintEvent *event)
|
void PlaybackProgressIndicator::paintEvent(QPaintEvent *event)
|
||||||
{
|
{
|
||||||
constexpr int progressBarHeight = 6;
|
constexpr int progressBarHeight = 6;
|
||||||
@ -66,6 +155,7 @@ void PlaybackProgressIndicator::paintEvent(QPaintEvent *event)
|
|||||||
painter.setPen(Qt::lightGray);
|
painter.setPen(Qt::lightGray);
|
||||||
for (int i = 0; i < m_chapterModel.rowCount(); i++) {
|
for (int i = 0; i < m_chapterModel.rowCount(); i++) {
|
||||||
qint64 chapterStartTime = m_chapterModel.item(i)->data(StartTimeMsRole).toInt();
|
qint64 chapterStartTime = m_chapterModel.item(i)->data(StartTimeMsRole).toInt();
|
||||||
|
if (chapterStartTime == 0) continue;
|
||||||
if (chapterStartTime > m_duration) break;
|
if (chapterStartTime > m_duration) break;
|
||||||
float chapterPercent = chapterStartTime / (float)m_duration;
|
float chapterPercent = chapterStartTime / (float)m_duration;
|
||||||
float chapterPosX = width() * chapterPercent;
|
float chapterPosX = width() * chapterPercent;
|
||||||
|
@ -28,6 +28,10 @@ public:
|
|||||||
void setDuration(qint64 dur);
|
void setDuration(qint64 dur);
|
||||||
void setChapters(QList<std::pair<qint64, QString>> chapters);
|
void setChapters(QList<std::pair<qint64, QString>> chapters);
|
||||||
|
|
||||||
|
static QList<std::pair<qint64, QString>> tryLoadSidecarChapterFile(const QString & filePath);
|
||||||
|
static QList<std::pair<qint64, QString>> parseCHPChapterFile(const QString & filePath);
|
||||||
|
static QList<std::pair<qint64, QString>> parsePBFChapterFile(const QString & filePath);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void seekOnMoveChanged(bool sow);
|
void seekOnMoveChanged(bool sow);
|
||||||
void positionChanged(qint64 newPosition);
|
void positionChanged(qint64 newPosition);
|
||||||
|
Loading…
Reference in New Issue
Block a user