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"