File indexing completed on 2024-05-05 04:50:34

0001 /*
0002    SPDX-FileCopyrightText: 2021 Han Young <hanyoung@protonmail.com>
0003 
0004    SPDX-License-Identifier: LGPL-3.0-or-later
0005  */
0006 #include "lyricsmodel.h"
0007 #include <algorithm>
0008 #include <unordered_map>
0009 #include <KLocalizedString>
0010 class LyricsModel::LyricsModelPrivate
0011 {
0012 public:
0013     bool parse(const QString &lyric);
0014     int highlightedIndex{-1};
0015     bool isLRC {false};
0016 
0017     std::vector<std::pair<QString, qint64>> lyrics;
0018 
0019 private:
0020     qint64 parseOneTimeStamp(QString::const_iterator &begin, QString::const_iterator end);
0021     QString parseOneLine(QString::const_iterator &begin, QString::const_iterator end);
0022     QString parseTags(QString::const_iterator &begin, QString::const_iterator end);
0023 
0024     qint64 offset = 0;
0025 };
0026 /*###########parseOneTimeStamp###########
0027  * Function to parse timestamp of one LRC line
0028  * if successful, return timestamp in milliseconds
0029  * otherwise return -1
0030  * */
0031 qint64 LyricsModel::LyricsModelPrivate::parseOneTimeStamp(
0032     QString::const_iterator &begin,
0033     QString::const_iterator end)
0034 {
0035     /* Example of LRC format and corresponding states
0036      *
0037      * States:
0038      *
0039      *  [00:01.02]bla bla
0040      * ^^^ ^^ ^^ ^^
0041      * ||| || || ||
0042      * ||| || || |End
0043      * ||| || || RightBracket
0044      * ||| || |Hundredths
0045      * ||| || Period
0046      * ||| |Seconds
0047      * ||| Colon
0048      * ||Minutes
0049      * |LeftBracket
0050      * Start
0051      * */
0052     enum States {Start, LeftBracket, Minutes, Colon, Seconds, Period, Hundredths, RightBracket, End};
0053     auto states {States::Start};
0054     auto minute {0}, second {0}, hundred {0};
0055 
0056     while (begin != end) {
0057         switch (begin->toLatin1()) {
0058         case '.':
0059             if (states == Seconds)
0060                 states = Period;
0061             break;
0062         case '[':
0063             if (states == Start)
0064                 states = LeftBracket;
0065             break;
0066         case ']':
0067             begin++;
0068             if (states == Hundredths) {
0069                 return minute * 60 * 1000 + second * 1000 +
0070                     hundred * 10; // we return milliseconds
0071             }
0072             else {
0073                 return -1;
0074             }
0075         case ':':
0076             if (states == Minutes)
0077                 states = Colon;
0078             break;
0079         default:
0080             if (begin->isDigit()) {
0081                 switch (states) {
0082                     case LeftBracket:
0083                         states = Minutes;
0084                         [[fallthrough]];
0085                     case Minutes:
0086                         minute *= 10;
0087                         minute += begin->digitValue();
0088                         break;
0089                     case Colon:
0090                         states = Seconds;
0091                         [[fallthrough]];
0092                     case Seconds:
0093                         second *= 10;
0094                         second += begin->digitValue();
0095                         break;
0096                     case Period:
0097                         states = Hundredths;
0098                         [[fallthrough]];
0099                     case Hundredths:
0100                         // we only parse to hundredth second
0101                         if (hundred >= 100) {
0102                             break;
0103                         }
0104                         hundred *= 10;
0105                         hundred += begin->digitValue();
0106                         break;
0107                     default:
0108                         // lyric format is corrupt
0109                         break;
0110                 }
0111             } else {
0112                 begin++;
0113                 return -1;
0114             }
0115             break;
0116         }
0117         begin++;
0118     }
0119 
0120     // end of lyric and no correct value found
0121     return -1;
0122 }
0123 
0124 QString
0125 LyricsModel::LyricsModelPrivate::parseOneLine(QString::const_iterator &begin,
0126                                               QString::const_iterator end)
0127 {
0128     auto size{0};
0129     auto it = begin;
0130     while (begin != end) {
0131         if (begin->toLatin1() != '[') {
0132             size++;
0133         } else
0134             break;
0135         begin++;
0136     }
0137     if (size) {
0138         return QString(--it, size); // FIXME: really weird workaround for QChar,
0139                                     // otherwise first char is lost
0140     } else
0141         return {};
0142 }
0143 
0144 /*
0145  * [length:04:07.46]
0146  * [re:www.megalobiz.com/lrc/maker]
0147  * [ve:v1.2.3]
0148  */
0149 QString LyricsModel::LyricsModelPrivate::parseTags(QString::const_iterator &begin, QString::const_iterator end)
0150 {
0151     static auto skipTillChar = [](QString::const_iterator begin, QString::const_iterator end, char endChar) {
0152         while (begin != end && begin->toLatin1() != endChar) {
0153             begin++;
0154         }
0155         return begin;
0156     };
0157     static std::unordered_map<QString, QString> map = {
0158         {QStringLiteral("ar"), i18nc("@label musical artist", "Artist")},
0159         {QStringLiteral("al"), i18nc("@label musical album", "Album")},
0160         {QStringLiteral("ti"), i18nc("@label song title", "Title")},
0161         {QStringLiteral("au"), i18nc("@label", "Creator")},
0162         {QStringLiteral("length"), i18nc("@label song length", "Length")},
0163         {QStringLiteral("by"), i18nc("@label as in 'Created by: Joe'", "Created by")},
0164         {QStringLiteral("re"), i18nc("@label as in 'a person who edits'", "Editor")},
0165         {QStringLiteral("ve"), i18nc("@label", "Version")}};
0166     QString tags;
0167 
0168     while (begin != end) {
0169         // skip till tags
0170         begin = skipTillChar(begin, end, '[');
0171         if (begin != end) {
0172             begin++;
0173         }
0174         else {
0175             break;
0176         }
0177 
0178         auto tagIdEnd = skipTillChar(begin, end, ':');
0179         auto tagId = QString(begin, std::distance(begin, tagIdEnd));
0180         if (tagIdEnd != end &&
0181             (map.count(tagId) || tagId == QStringLiteral("offset"))) {
0182           tagIdEnd++;
0183 
0184           auto tagContentEnd = skipTillChar(tagIdEnd, end, ']');
0185           bool ok = true;
0186           if (map.count(tagId)) {
0187             tags += i18nc(
0188                 "@label this is a key => value map", "%1: %2\n", map[tagId],
0189                 QString(tagIdEnd, std::distance(tagIdEnd, tagContentEnd)));
0190           } else {
0191             // offset tag
0192             offset = QString(tagIdEnd, std::distance(tagIdEnd, tagContentEnd))
0193                          .toLongLong(&ok);
0194           }
0195 
0196           if (ok) {
0197             begin = tagContentEnd;
0198           } else {
0199             // Invalid offset tag, we step back one to compensate the '[' we
0200             // step over
0201             begin--;
0202             break;
0203           }
0204         } else {
0205           // No tag, we step back one to compensate the '[' we step over
0206           begin--;
0207           break;
0208         }
0209     }
0210     return tags;
0211 }
0212 
0213 bool LyricsModel::LyricsModelPrivate::parse(const QString &lyric)
0214 {
0215     lyrics.clear();
0216     offset = 0;
0217 
0218     if (lyric.isEmpty())
0219         return false;
0220 
0221     QString::const_iterator begin = lyric.begin(), end = lyric.end();
0222     auto tag = parseTags(begin, end);
0223     std::vector<qint64> timeStamps;
0224 
0225     while (begin != lyric.end()) {
0226         auto timeStamp = parseOneTimeStamp(begin, end);
0227         while (timeStamp >= 0) {
0228           // one line can have multiple timestamps
0229           // [00:12.00][00:15.30]Some more lyrics ...
0230           timeStamps.push_back(timeStamp);
0231           timeStamp = parseOneTimeStamp(begin, end);
0232         }
0233         auto string = parseOneLine(begin, end);
0234         if (!string.isEmpty() && !timeStamps.empty()) {
0235             for (auto time : timeStamps) {
0236                 lyrics.push_back({string, time});
0237             }
0238         }
0239         timeStamps.clear();
0240     }
0241 
0242     std::sort(lyrics.begin(),
0243               lyrics.end(),
0244               [](const std::pair<QString, qint64> &lhs,
0245                  const std::pair<QString, qint64> &rhs) {
0246                   return lhs.second < rhs.second;
0247               });
0248     if (offset) {
0249       std::transform(lyrics.begin(), lyrics.end(),
0250                      lyrics.begin(),
0251                      [this](std::pair<QString, qint64> &element) {
0252                        element.second = std::max(element.second + offset, 0ll);
0253                        return element;
0254                      });
0255     }
0256     // insert tags to first lyric front
0257     if (!lyrics.empty() && !tag.isEmpty()) {
0258         lyrics.insert(lyrics.begin(), {tag, 0});
0259     }
0260     return !lyrics.empty();
0261 }
0262 
0263 LyricsModel::LyricsModel(QObject *parent)
0264     : QAbstractListModel(parent)
0265     , d(std::make_unique<LyricsModelPrivate>())
0266 {
0267 }
0268 
0269 LyricsModel::~LyricsModel() = default;
0270 
0271 int LyricsModel::rowCount(const QModelIndex &parent) const
0272 {
0273     Q_UNUSED(parent)
0274     return d->lyrics.size();
0275 }
0276 
0277 QVariant LyricsModel::data(const QModelIndex &index, int role) const
0278 {
0279     if (index.row() < 0 || index.row() >= (int)d->lyrics.size())
0280         return {};
0281 
0282     switch (role) {
0283     case LyricsRole::Lyric:
0284         return d->lyrics.at(index.row()).first;
0285     case LyricsRole::TimeStamp:
0286         return d->lyrics.at(index.row()).second;
0287     }
0288 
0289     return QVariant();
0290 }
0291 
0292 void LyricsModel::setLyric(const QString &lyric)
0293 {
0294     bool isLRC = true;
0295 
0296     beginResetModel();
0297     auto ret = d->parse(lyric);
0298 
0299     // has non-LRC formatted lyric
0300     if (!ret && !lyric.isEmpty()) {
0301         d->lyrics = {{lyric, 0ll}};
0302         d->highlightedIndex = -1;
0303         isLRC = false;
0304     }
0305     endResetModel();
0306 
0307     Q_EMIT highlightedIndexChanged();
0308     Q_EMIT lyricChanged();
0309     if (isLRC != d->isLRC) {
0310         d->isLRC = isLRC;
0311         Q_EMIT isLRCChanged();
0312     }
0313 }
0314 
0315 void LyricsModel::setPosition(qint64 position)
0316 {
0317     if (!isLRC()) {
0318         return;
0319     }
0320 
0321     // do binary search
0322     auto result =
0323         std::lower_bound(d->lyrics.begin(),
0324                          d->lyrics.end(),
0325                          position,
0326                          [](const std::pair<QString, qint64> &lhs, qint64 value) {
0327                              return lhs.second < value;
0328                          });
0329     if (result != d->lyrics.begin()) {
0330         d->highlightedIndex = std::distance(d->lyrics.begin(), --result);
0331     } else {
0332         d->highlightedIndex = -1;
0333     }
0334     Q_EMIT highlightedIndexChanged();
0335 }
0336 
0337 int LyricsModel::highlightedIndex() const
0338 {
0339     return d->highlightedIndex;
0340 }
0341 
0342 bool LyricsModel::isLRC() const
0343 {
0344     return d->isLRC;
0345 }
0346 
0347 QHash<int, QByteArray> LyricsModel::roleNames() const
0348 {
0349     return {{LyricsRole::Lyric, QByteArrayLiteral("lyric")}, {LyricsRole::TimeStamp, QByteArrayLiteral("timestamp")}};
0350 }
0351 
0352 #include "moc_lyricsmodel.cpp"