/* SPDX-FileCopyrightText: 2021 Han Young SPDX-License-Identifier: LGPL-3.0-or-later */ #include "lyricsmodel.h" #include #include #include class LyricsModel::LyricsModelPrivate { public: bool parse(const QString &lyric); int highlightedIndex{-1}; bool isLRC {false}; std::vector> lyrics; private: qint64 parseOneTimeStamp(QString::const_iterator &begin, QString::const_iterator end); QString parseOneLine(QString::const_iterator &begin, QString::const_iterator end); QString parseTags(QString::const_iterator &begin, QString::const_iterator end); qint64 offset = 0; }; /*###########parseOneTimeStamp########### * Function to parse timestamp of one LRC line * if successful, return timestamp in milliseconds * otherwise return -1 * */ qint64 LyricsModel::LyricsModelPrivate::parseOneTimeStamp( QString::const_iterator &begin, QString::const_iterator end) { /* Example of LRC format and corresponding states * * States: * * [00:01.02]bla bla * ^^^ ^^ ^^ ^^ * ||| || || || * ||| || || |End * ||| || || RightBracket * ||| || |Hundredths * ||| || Period * ||| |Seconds * ||| Colon * ||Minutes * |LeftBracket * Start * */ enum States {Start, LeftBracket, Minutes, Colon, Seconds, Period, Hundredths, RightBracket, End}; auto states {States::Start}; auto minute {0}, second {0}, hundred {0}; while (begin != end) { switch (begin->toLatin1()) { case '.': if (states == Seconds) states = Period; break; case '[': if (states == Start) states = LeftBracket; break; case ']': begin++; if (states == Hundredths) { return minute * 60 * 1000 + second * 1000 + hundred * 10; // we return milliseconds } else { return -1; } case ':': if (states == Minutes) states = Colon; break; default: if (begin->isDigit()) { switch (states) { case LeftBracket: states = Minutes; [[fallthrough]]; case Minutes: minute *= 10; minute += begin->digitValue(); break; case Colon: states = Seconds; [[fallthrough]]; case Seconds: second *= 10; second += begin->digitValue(); break; case Period: states = Hundredths; [[fallthrough]]; case Hundredths: // we only parse to hundredth second if (hundred >= 100) { break; } hundred *= 10; hundred += begin->digitValue(); break; default: // lyric format is corrupt break; } } else { begin++; return -1; } break; } begin++; } // end of lyric and no correct value found return -1; } QString LyricsModel::LyricsModelPrivate::parseOneLine(QString::const_iterator &begin, QString::const_iterator end) { auto size{0}; auto it = begin; while (begin != end) { if (begin->toLatin1() != '[') { size++; } else break; begin++; } if (size) { return QString(--it, size); // FIXME: really weird workaround for QChar, // otherwise first char is lost } else return {}; } /* * [length:04:07.46] * [re:www.megalobiz.com/lrc/maker] * [ve:v1.2.3] */ QString LyricsModel::LyricsModelPrivate::parseTags(QString::const_iterator &begin, QString::const_iterator end) { static auto skipTillChar = [](QString::const_iterator begin, QString::const_iterator end, char endChar) { while (begin != end && begin->toLatin1() != endChar) { begin++; } return begin; }; static std::unordered_map map = { {QStringLiteral("ar"), i18nc("@label musical artist", "Artist")}, {QStringLiteral("al"), i18nc("@label musical album", "Album")}, {QStringLiteral("ti"), i18nc("@label song title", "Title")}, {QStringLiteral("au"), i18nc("@label", "Creator")}, {QStringLiteral("length"), i18nc("@label song length", "Length")}, {QStringLiteral("by"), i18nc("@label as in 'Created by: Joe'", "Created by")}, {QStringLiteral("re"), i18nc("@label as in 'a person who edits'", "Editor")}, {QStringLiteral("ve"), i18nc("@label", "Version")}}; QString tags; while (begin != end) { // skip till tags begin = skipTillChar(begin, end, '['); if (begin != end) { begin++; } else { break; } auto tagIdEnd = skipTillChar(begin, end, ':'); auto tagId = QString(begin, std::distance(begin, tagIdEnd)); if (tagIdEnd != end && (map.count(tagId) || tagId == QStringLiteral("offset"))) { tagIdEnd++; auto tagContentEnd = skipTillChar(tagIdEnd, end, ']'); bool ok = true; if (map.count(tagId)) { tags += i18nc( "@label this is a key => value map", "%1: %2\n", map[tagId], QString(tagIdEnd, std::distance(tagIdEnd, tagContentEnd))); } else { // offset tag offset = QString(tagIdEnd, std::distance(tagIdEnd, tagContentEnd)) .toLongLong(&ok); } if (ok) { begin = tagContentEnd; } else { // Invalid offset tag, we step back one to compensate the '[' we // step over begin--; break; } } else { // No tag, we step back one to compensate the '[' we step over begin--; break; } } return tags; } bool LyricsModel::LyricsModelPrivate::parse(const QString &lyric) { lyrics.clear(); offset = 0; if (lyric.isEmpty()) return false; QString::const_iterator begin = lyric.begin(), end = lyric.end(); auto tag = parseTags(begin, end); std::vector timeStamps; while (begin != lyric.end()) { auto timeStamp = parseOneTimeStamp(begin, end); while (timeStamp >= 0) { // one line can have multiple timestamps // [00:12.00][00:15.30]Some more lyrics ... timeStamps.push_back(timeStamp); timeStamp = parseOneTimeStamp(begin, end); } auto string = parseOneLine(begin, end); if (!string.isEmpty() && !timeStamps.empty()) { for (auto time : timeStamps) { lyrics.push_back({string, time}); } } timeStamps.clear(); } std::sort(lyrics.begin(), lyrics.end(), [](const std::pair &lhs, const std::pair &rhs) { return lhs.second < rhs.second; }); if (offset) { std::transform(lyrics.begin(), lyrics.end(), lyrics.begin(), [this](std::pair &element) { element.second = std::max(element.second + offset, 0ll); return element; }); } // insert tags to first lyric front if (!lyrics.empty() && !tag.isEmpty()) { lyrics.insert(lyrics.begin(), {tag, 0}); } return !lyrics.empty(); } LyricsModel::LyricsModel(QObject *parent) : QAbstractListModel(parent) , d(std::make_unique()) { } LyricsModel::~LyricsModel() = default; int LyricsModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return d->lyrics.size(); } QVariant LyricsModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= (int)d->lyrics.size()) return {}; switch (role) { case LyricsRole::Lyric: return d->lyrics.at(index.row()).first; case LyricsRole::TimeStamp: return d->lyrics.at(index.row()).second; } return QVariant(); } void LyricsModel::setLyric(const QString &lyric) { bool isLRC = true; beginResetModel(); auto ret = d->parse(lyric); // has non-LRC formatted lyric if (!ret && !lyric.isEmpty()) { d->lyrics = {{lyric, 0ll}}; d->highlightedIndex = -1; isLRC = false; } endResetModel(); Q_EMIT highlightedIndexChanged(); Q_EMIT lyricChanged(); if (isLRC != d->isLRC) { d->isLRC = isLRC; Q_EMIT isLRCChanged(); } } void LyricsModel::setPosition(qint64 position) { if (!isLRC()) { return; } // do binary search auto result = std::lower_bound(d->lyrics.begin(), d->lyrics.end(), position, [](const std::pair &lhs, qint64 value) { return lhs.second < value; }); if (result != d->lyrics.begin()) { d->highlightedIndex = std::distance(d->lyrics.begin(), --result); } else { d->highlightedIndex = -1; } Q_EMIT highlightedIndexChanged(); } int LyricsModel::highlightedIndex() const { return d->highlightedIndex; } bool LyricsModel::isLRC() const { return d->isLRC; } QHash LyricsModel::roleNames() const { return {{LyricsRole::Lyric, QByteArrayLiteral("lyric")}, {LyricsRole::TimeStamp, QByteArrayLiteral("timestamp")}}; } #include "moc_lyricsmodel.cpp"