From f17b722600c17540e22c918aa901f899624d4204 Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Thu, 23 Jan 2025 22:45:46 +0800 Subject: [PATCH] feat: sidecar .chp and .pbf support --- README.md | 5 +- mainwindow.cpp | 19 ++++++-- playbackprogressindicator.cpp | 90 +++++++++++++++++++++++++++++++++++ playbackprogressindicator.h | 4 ++ 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ae2780d..39c12a8 100644 --- a/README.md +++ b/README.md @@ -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: - [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 -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). - ...which if you use Qt's official binary, only contains the LGPLv2.1+ part. (already good enough, tho) diff --git a/mainwindow.cpp b/mainwindow.cpp index d501044..755f1c2 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -242,6 +242,12 @@ void MainWindow::dropEvent(QDropEvent *e) return; } + if (fileName.endsWith(".chp") || fileName.endsWith(".pbf")) { + QList> chapters(PlaybackProgressIndicator::tryLoadSidecarChapterFile(fileName)); + ui->playbackProgressIndicator->setChapters(chapters); + return; + } + const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urls); if (modelIndex.isValid()) { loadByModelIndex(modelIndex); @@ -264,14 +270,21 @@ void MainWindow::loadFile() } m_playlistManager->loadPlaylist(urlList); - m_mediaPlayer->setSource(urlList.first()); - m_lrcbar->loadLyrics(urlList.first().toLocalFile()); + const QUrl & firstUrl = urlList.first(); + const QString firstFilePath = firstUrl.toLocalFile(); + m_mediaPlayer->setSource(firstUrl); + m_lrcbar->loadLyrics(firstFilePath); + QList> chapters(PlaybackProgressIndicator::tryLoadSidecarChapterFile(firstFilePath)); + ui->playbackProgressIndicator->setChapters(chapters); } void MainWindow::loadByModelIndex(const QModelIndex & 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> chapters(PlaybackProgressIndicator::tryLoadSidecarChapterFile(filePath)); + ui->playbackProgressIndicator->setChapters(chapters); } void MainWindow::play() diff --git a/playbackprogressindicator.cpp b/playbackprogressindicator.cpp index ca781a7..77bfd25 100644 --- a/playbackprogressindicator.cpp +++ b/playbackprogressindicator.cpp @@ -4,8 +4,12 @@ #include "playbackprogressindicator.h" +#include +#include +#include #include #include +#include PlaybackProgressIndicator::PlaybackProgressIndicator(QWidget *parent) : QWidget(parent) @@ -35,6 +39,91 @@ void PlaybackProgressIndicator::setChapters(QList > c update(); } +QList > 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 > PlaybackProgressIndicator::parseCHPChapterFile(const QString &filePath) +{ + QList> 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 > PlaybackProgressIndicator::parsePBFChapterFile(const QString &filePath) +{ + QList> 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) { constexpr int progressBarHeight = 6; @@ -66,6 +155,7 @@ void PlaybackProgressIndicator::paintEvent(QPaintEvent *event) painter.setPen(Qt::lightGray); for (int i = 0; i < m_chapterModel.rowCount(); i++) { qint64 chapterStartTime = m_chapterModel.item(i)->data(StartTimeMsRole).toInt(); + if (chapterStartTime == 0) continue; if (chapterStartTime > m_duration) break; float chapterPercent = chapterStartTime / (float)m_duration; float chapterPosX = width() * chapterPercent; diff --git a/playbackprogressindicator.h b/playbackprogressindicator.h index 0959738..44e048b 100644 --- a/playbackprogressindicator.h +++ b/playbackprogressindicator.h @@ -28,6 +28,10 @@ public: void setDuration(qint64 dur); void setChapters(QList> chapters); + static QList> tryLoadSidecarChapterFile(const QString & filePath); + static QList> parseCHPChapterFile(const QString & filePath); + static QList> parsePBFChapterFile(const QString & filePath); + signals: void seekOnMoveChanged(bool sow); void positionChanged(qint64 newPosition);