2024-09-20 20:59:40 +08:00
|
|
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
|
|
|
//
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
#include "lyricsmanager.h"
|
|
|
|
|
|
|
|
#include <QDir>
|
|
|
|
#include <QFileInfo>
|
|
|
|
#include <QRegularExpression>
|
2024-09-22 14:27:53 +08:00
|
|
|
#include <QStringConverter>
|
|
|
|
|
2024-09-28 23:46:00 +08:00
|
|
|
#ifdef HAVE_KCODECS
|
2024-09-28 12:56:49 +08:00
|
|
|
#include <KCharsets>
|
|
|
|
#include <KCodecs>
|
|
|
|
#include <KEncodingProber>
|
2024-09-22 14:27:53 +08:00
|
|
|
#endif
|
|
|
|
|
2024-09-28 23:46:00 +08:00
|
|
|
#ifdef USE_QTEXTCODEC
|
|
|
|
#include <QTextCodec>
|
|
|
|
#endif
|
|
|
|
|
2024-09-22 14:27:53 +08:00
|
|
|
Q_LOGGING_CATEGORY(lcLyrics, "pmusic.lyrics")
|
|
|
|
Q_LOGGING_CATEGORY(lcLyricsParser, "pmusic.lyrics.parser")
|
2024-09-20 20:59:40 +08:00
|
|
|
|
|
|
|
LyricsManager::LyricsManager(QObject *parent)
|
|
|
|
: QObject(parent)
|
|
|
|
{
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
LyricsManager::~LyricsManager()
|
|
|
|
{
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LyricsManager::loadLyrics(QString filepath)
|
|
|
|
{
|
|
|
|
// reset state
|
|
|
|
reset();
|
|
|
|
|
|
|
|
// check and load file
|
|
|
|
QFileInfo fileInfo(filepath);
|
|
|
|
if (!filepath.endsWith(".lrc", Qt::CaseInsensitive)) {
|
|
|
|
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".lrc"));
|
|
|
|
}
|
|
|
|
if (!fileInfo.exists()) return false;
|
|
|
|
|
|
|
|
QFile file(fileInfo.absoluteFilePath());
|
|
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
|
|
return false;
|
|
|
|
}
|
2024-09-22 14:27:53 +08:00
|
|
|
QByteArray fileContent(file.readAll());
|
2024-09-28 23:46:00 +08:00
|
|
|
QStringList lines;
|
|
|
|
#ifdef HAVE_KCODECS
|
2024-09-28 12:56:49 +08:00
|
|
|
KEncodingProber prober(KEncodingProber::Universal);
|
|
|
|
prober.feed(fileContent);
|
|
|
|
QByteArray encoding(prober.encoding());
|
|
|
|
qCDebug(lcLyrics) << "Detected encoding:" << QString(encoding) << "with confidence" << prober.confidence();
|
2024-09-28 23:46:00 +08:00
|
|
|
#ifdef USE_QTEXTCODEC
|
|
|
|
qCDebug(lcLyrics) << "QTextCodec is used instead of QStringConverter.";
|
|
|
|
QTextCodec *codec = QTextCodec::codecForName(encoding);
|
|
|
|
if (codec) {
|
|
|
|
lines = codec->toUnicode(fileContent).split('\n');
|
|
|
|
} else {
|
|
|
|
lines = QString(fileContent).split('\n');
|
|
|
|
qCDebug(lcLyrics) << "No codec for the detected encoding. Available codecs are:" << QTextCodec::availableCodecs();
|
|
|
|
qCDebug(lcLyrics) << "KCodecs offers these encodings:" << KCharsets::charsets()->availableEncodingNames();
|
|
|
|
}
|
|
|
|
#else // NOT USE_QTEXTCODEC
|
2024-09-28 12:56:49 +08:00
|
|
|
auto toUtf16 = QStringDecoder(encoding);
|
|
|
|
// Don't use `QStringConverter::availableCodecs().contains(QString(encoding))` here, since the charset
|
|
|
|
// encoding name might not match, e.g. GB18030 (from availableCodecs) != gb18030 (from KEncodingProber)
|
|
|
|
if (toUtf16.isValid()) {
|
2024-09-22 14:27:53 +08:00
|
|
|
QString decodedResult = toUtf16(fileContent);
|
|
|
|
lines = decodedResult.split('\n');
|
|
|
|
} else {
|
2024-09-28 12:56:49 +08:00
|
|
|
qCDebug(lcLyrics) << "No codec for the detected encoding. Available codecs are:" << QStringConverter::availableCodecs();
|
|
|
|
qCDebug(lcLyrics) << "KCodecs offers these encodings:" << KCharsets::charsets()->availableEncodingNames();
|
2024-09-22 14:27:53 +08:00
|
|
|
lines = QString(fileContent).split('\n');
|
|
|
|
}
|
2024-09-28 23:46:00 +08:00
|
|
|
#endif // USE_QTEXTCODEC
|
|
|
|
#else // NOT HAVE_KCODECS
|
|
|
|
lines = QString(fileContent).split('\n');
|
|
|
|
#endif // HAVE_KCODECS
|
2024-09-20 20:59:40 +08:00
|
|
|
file.close();
|
|
|
|
|
|
|
|
// parse lyrics timestamp
|
|
|
|
QRegularExpression tagRegex(R"regex(\[(ti|ar|al|au|length|by|offset|tool|re|ve|#):\s?([^\]]*)\]$)regex");
|
2024-09-25 19:50:50 +08:00
|
|
|
QRegularExpression lrcRegex(R"regex(\[(\d{2,3}:\d{2}\.\d{2,3})\](.*))regex");
|
2024-09-20 20:59:40 +08:00
|
|
|
bool tagSectionPassed = false;
|
|
|
|
|
|
|
|
for (QString line : std::as_const(lines)) {
|
|
|
|
line = line.trimmed();
|
|
|
|
if (line.isEmpty()) continue;
|
|
|
|
|
|
|
|
if (!tagSectionPassed) {
|
|
|
|
QRegularExpressionMatch tagMatch = tagRegex.match(line);
|
|
|
|
if (tagMatch.hasMatch()) {
|
|
|
|
QString tag(tagMatch.captured(1));
|
|
|
|
if (tag == QLatin1String("offset")) {
|
|
|
|
// The value is prefixed with either + or -, with + causing lyrics to appear sooner
|
|
|
|
m_timeOffset = -tagMatch.captured(2).toInt();
|
2024-09-22 14:27:53 +08:00
|
|
|
qCDebug(lcLyricsParser) << m_timeOffset;
|
2024-09-20 20:59:40 +08:00
|
|
|
}
|
2024-09-22 14:27:53 +08:00
|
|
|
qCDebug(lcLyricsParser) << "[tag]" << tagMatch.captured(1) << tagMatch.captured(2);
|
2024-09-20 20:59:40 +08:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-22 14:27:53 +08:00
|
|
|
QList<int> timestamps;
|
|
|
|
QString currentLrc;
|
2024-09-20 20:59:40 +08:00
|
|
|
QRegularExpressionMatch match = lrcRegex.match(line);
|
2024-09-22 14:27:53 +08:00
|
|
|
while (match.hasMatch()) {
|
2024-09-20 20:59:40 +08:00
|
|
|
tagSectionPassed = true;
|
|
|
|
QTime timestamp(QTime::fromString(match.captured(1), "m:s.zz"));
|
2024-09-22 14:27:53 +08:00
|
|
|
timestamps.append(timestamp.msecsSinceStartOfDay());
|
|
|
|
currentLrc = match.captured(2);
|
|
|
|
match = lrcRegex.match(currentLrc);
|
|
|
|
}
|
|
|
|
if (!timestamps.isEmpty()) {
|
|
|
|
for (int timestamp : std::as_const(timestamps)) {
|
|
|
|
m_lyricsMap.insert(timestamp, currentLrc);
|
|
|
|
qCDebug(lcLyricsParser) << "[lrc]" << timestamp << currentLrc;
|
|
|
|
}
|
2024-09-20 20:59:40 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!m_lyricsMap.isEmpty()) {
|
|
|
|
m_timestampList = m_lyricsMap.keys();
|
|
|
|
std::sort(m_timestampList.begin(), m_timestampList.end());
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LyricsManager::hasLyrics() const
|
|
|
|
{
|
|
|
|
return !m_lyricsMap.isEmpty();
|
|
|
|
}
|
|
|
|
|
|
|
|
void LyricsManager::updateCurrentTimeMs(int curTimeMs, int totalTimeMs)
|
|
|
|
{
|
|
|
|
if (!hasLyrics()) return;
|
|
|
|
|
|
|
|
// TODO: we don't need to find from the top everytime the time is updated
|
2024-09-26 00:03:26 +08:00
|
|
|
auto iter = std::find_if(m_timestampList.begin(), m_timestampList.end(), [&curTimeMs, this](int timestamp) -> bool {
|
2024-09-20 20:59:40 +08:00
|
|
|
return (timestamp + m_timeOffset) > curTimeMs;
|
|
|
|
});
|
|
|
|
|
2024-09-26 00:03:26 +08:00
|
|
|
m_nextLyricsTime = iter == m_timestampList.end() ? totalTimeMs : *iter;
|
|
|
|
if (iter != m_timestampList.begin()) {
|
2024-09-20 20:59:40 +08:00
|
|
|
iter--;
|
|
|
|
}
|
|
|
|
m_currentLyricsTime = *iter;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString LyricsManager::lyrics(int lineOffset) const
|
|
|
|
{
|
|
|
|
if (!hasLyrics()) return QString();
|
|
|
|
|
|
|
|
int index = m_timestampList.indexOf(m_currentLyricsTime) + lineOffset;
|
|
|
|
if (index >= 0 && index < m_timestampList.count()) {
|
|
|
|
int timestamp = m_timestampList.at(index);
|
|
|
|
return m_lyricsMap.value(timestamp);
|
|
|
|
} else {
|
|
|
|
return QString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
double LyricsManager::maskPercent(int curTimeMs)
|
|
|
|
{
|
|
|
|
if (!hasLyrics()) return 0;
|
|
|
|
if (curTimeMs <= currentLyricsTime()) return 0;
|
|
|
|
if (curTimeMs >= nextLyricsTime()) return 1;
|
|
|
|
if (m_nextLyricsTime == currentLyricsTime()) return 1;
|
|
|
|
|
|
|
|
return (double)(curTimeMs - currentLyricsTime()) / (m_nextLyricsTime - m_currentLyricsTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
void LyricsManager::reset()
|
|
|
|
{
|
|
|
|
m_currentLyricsTime = 0;
|
|
|
|
m_nextLyricsTime = 0;
|
|
|
|
m_timeOffset = 0;
|
|
|
|
m_lyricsMap.clear();
|
|
|
|
m_timestampList.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
int LyricsManager::currentLyricsTime() const
|
|
|
|
{
|
|
|
|
return m_currentLyricsTime + m_timeOffset;
|
|
|
|
}
|
|
|
|
|
|
|
|
int LyricsManager::nextLyricsTime() const
|
|
|
|
{
|
|
|
|
return m_nextLyricsTime + m_timeOffset;
|
|
|
|
}
|