MythTV master
lyricsdata.cpp
Go to the documentation of this file.
1#include <iostream>
2
3#include "musicmetadata.h"
4
5// qt
6#include <QRegularExpression>
7#include <QDomDocument>
8
9// mythtv
13
14// libmythmetadata
15#include "lyricsdata.h"
16
17static const QRegularExpression kTimeCode { R"(^(\[(\d\d):(\d\d)(?:\.(\d\d))?\])(.*))" };
18
19/*************************************************************************/
20//LyricsData
21
23{
24 clear();
25}
26
28{
30
32
33 m_syncronized = false;
34 m_changed = false;
36}
37
39{
40 auto i = m_lyricsMap.begin();
41 while (i != m_lyricsMap.end())
42 {
43 delete i.value();
44 ++i;
45 }
46
47 m_lyricsMap.clear();
48}
49
50void LyricsData::findLyrics(const QString &grabber)
51{
52 if (!m_parent)
53 return;
54
55 switch (m_status)
56 {
58 {
59 emit statusChanged(m_status, tr("Searching..."));
60 return;
61 }
62
63 case STATUS_FOUND:
64 {
65 emit statusChanged(m_status, "");
66 return;
67 }
68
69 case STATUS_NOTFOUND:
70 {
71 emit statusChanged(m_status, tr("No lyrics found for this track"));
72 return;
73 }
74
75 default:
76 break;
77 }
78
79 clear();
80
82
83 // don't bother searching if we have no title, artist and album
84 if (m_parent->Title().isEmpty() && m_parent->Artist().isEmpty() && m_parent->Album().isEmpty())
85 {
87 emit statusChanged(m_status, tr("No lyrics found for this track"));
88 return;
89 }
90
91 // listen for messages
93
94 // send a message to the master BE to find the lyrics for this track
95 QStringList slist;
96 slist << "MUSIC_LYRICS_FIND"
97 << m_parent->Hostname()
98 << QString::number(m_parent->ID())
99 << grabber;
100
101 QString title = m_parent->Title().isEmpty() ? "*Unknown*" : m_parent->Title();
102 QString artist = m_parent->Artist().isEmpty() ? "*Unknown*" : m_parent->Artist();
103 QString album = m_parent->Album().isEmpty() ? "*Unknown*" : m_parent->Album();
104
105 if (!m_parent->isDBTrack())
106 {
107 slist << artist
108 << album
109 << title;
110 }
111
112 LOG(VB_NETWORK, LOG_INFO, QString("LyricsData:: Sending command %1").arg(slist.join('~')));
113
115}
116
118{
119 // only save the lyrics if they have been changed
120 if (!m_changed)
121 return;
122
123 // only save lyrics if it is a DB track
124 if (!m_parent || !m_parent->isDBTrack())
125 return;
126
127 // send a message to the master BE to save the lyrics for this track
128 QStringList slist;
129 slist << "MUSIC_LYRICS_SAVE"
130 << m_parent->Hostname()
131 << QString::number(m_parent->ID());
132
133 slist << createLyricsXML();
134
136}
137
139{
140 QDomDocument doc("lyrics");
141
142 QDomElement root = doc.createElement("lyrics");
143 doc.appendChild(root);
144
145 // artist
146 QDomElement artist = doc.createElement("artist");
147 root.appendChild(artist);
148 artist.appendChild(doc.createTextNode(m_artist));
149
150 // album
151 QDomElement album = doc.createElement("album");
152 root.appendChild(album);
153 album.appendChild(doc.createTextNode(m_album));
154
155 // title
156 QDomElement title = doc.createElement("title");
157 root.appendChild(title);
158 title.appendChild(doc.createTextNode(m_title));
159
160 // syncronized
161 QDomElement syncronized = doc.createElement("syncronized");
162 root.appendChild(syncronized);
163 syncronized.appendChild(doc.createTextNode(m_syncronized ? "True" : "False"));
164
165 // grabber
166 QDomElement grabber = doc.createElement("grabber");
167 root.appendChild(grabber);
168 grabber.appendChild(doc.createTextNode(m_grabber));
169
170 // lyrics
171 auto i = m_lyricsMap.begin();
172 while (i != m_lyricsMap.end())
173 {
174 LyricsLine *line = (*i);
175 QDomElement lyric = doc.createElement("lyric");
176 root.appendChild(lyric);
177 lyric.appendChild(doc.createTextNode(line->toString(m_syncronized)));
178 ++i;
179 }
180
181 return doc.toString(4);
182}
183
184void LyricsData::customEvent(QEvent *event)
185{
186 if (event->type() == MythEvent::kMythEventMessage)
187 {
188 auto *me = dynamic_cast<MythEvent*>(event);
189 if (!me)
190 return;
191
192 // we are only interested in MUSIC_LYRICS_* messages
193 if (me->Message().startsWith("MUSIC_LYRICS_"))
194 {
195 QStringList list = me->Message().simplified().split(' ');
196
197 if (list.size() >= 2)
198 {
199 uint songID = list[1].toUInt();
200
201 // make sure the message is for us
202 if (m_parent->ID() == songID)
203 {
204 if (list[0] == "MUSIC_LYRICS_FOUND")
205 {
207
208 QString xmlData = me->Message().section(" ", 2, -1);
209
210 // we found some lyrics so load them
211 loadLyrics(xmlData);
212 emit statusChanged(m_status, "");
213 }
214 else if (list[0] == "MUSIC_LYRICS_STATUS")
215 {
216 emit statusChanged(STATUS_SEARCHING, me->Message().section(" ", 2, -1));
217 }
218 else
219 {
221 // nothing found or an error occured
223 emit statusChanged(m_status, tr("No lyrics found for this track"));
224 }
225 }
226 }
227 }
228 }
229}
230
231void LyricsData::loadLyrics(const QString &xmlData)
232{
233 QDomDocument domDoc;
234#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
235 QString errorMsg;
236 int errorLine = 0;
237 int errorColumn = 0;
238
239 if (!domDoc.setContent(xmlData, false, &errorMsg, &errorLine, &errorColumn))
240 {
241 LOG(VB_GENERAL, LOG_ERR,
242 QString("LyricsData:: Could not parse lyrics from %1").arg(xmlData) +
243 QString("\n\t\t\tError at line: %1 column: %2 msg: %3").arg(errorLine).arg(errorColumn).arg(errorMsg));
245 return;
246 }
247#else
248 auto parseResult = domDoc.setContent(xmlData);
249 if (!parseResult)
250 {
251 LOG(VB_GENERAL, LOG_ERR,
252 QString("LyricsData:: Could not parse lyrics from %1").arg(xmlData) +
253 QString("\n\t\t\tError at line: %1 column: %2 msg: %3")
254 .arg(parseResult.errorLine).arg(parseResult.errorColumn)
255 .arg(parseResult.errorMessage));
257 return;
258 }
259#endif
260
261 QDomNodeList itemList = domDoc.elementsByTagName("lyrics");
262 QDomNode itemNode = itemList.item(0);
263
264 m_grabber = itemNode.namedItem(QString("grabber")).toElement().text();
265 m_artist = itemNode.namedItem(QString("artist")).toElement().text();
266 m_album = itemNode.namedItem(QString("album")).toElement().text();
267 m_title = itemNode.namedItem(QString("title")).toElement().text();
268 m_syncronized = (itemNode.namedItem(QString("syncronized")).toElement().text() == "True");
269 m_changed = false;
270
271 clearLyrics();
272
273 itemList = itemNode.toElement().elementsByTagName("lyric");
274
275 QStringList lyrics;
276
277 for (int x = 0; x < itemList.count(); x++)
278 {
279 QDomNode lyricNode = itemList.at(x);
280 QString lyric = lyricNode.toElement().text();
281
282 if (m_syncronized)
283 {
284 QStringList times;
285 auto match = kTimeCode.match(lyric);
286 if (match.hasMatch())
287 {
288 while (match.hasMatch())
289 {
290 times.append(lyric.left(match.capturedLength(1)));
291 lyric.remove(0,match.capturedLength(1));
292 match = kTimeCode.match(lyric);
293 }
294 for (const auto &time : std::as_const(times))
295 lyrics.append(time + lyric);
296 }
297 else
298 {
299 lyrics.append(lyric);
300 }
301 }
302 else
303 {
304 lyrics.append(lyric);
305 }
306 }
307
309
311}
312
313void LyricsData::setLyrics(const QStringList &lyrics)
314{
315 clearLyrics();
316
317 std::chrono::milliseconds lastTime = -1ms;
318 std::chrono::milliseconds offset = 0ms;
319
320 for (int x = 0; x < lyrics.count(); x++)
321 {
322 const QString& lyric = lyrics.at(x);
323
324 auto *line = new LyricsLine;
325
326 static const QRegularExpression kOffset { R"(^\[offset:(.+)\])" };
327 auto match = kOffset.match(lyric);
328 if (match.hasMatch())
329 offset = std::chrono::milliseconds(match.capturedView(1).toInt());
330
331 if (m_syncronized)
332 {
333 if (!lyric.isEmpty())
334 {
335 // does the line start with a time code like [12:34] or [12:34.56]
336 match = kTimeCode.match(lyric);
337 if (match.hasMatch())
338 {
339 int minutes = match.capturedView(2).toInt();
340 int seconds = match.capturedView(3).toInt();
341 int hundredths = match.capturedView(4).toInt();
342
343 line->m_lyric = match.captured(5);
344 line->m_time = millisecondsFromParts(0, minutes, seconds, hundredths * 10);
345 line->m_time = std::max(0ms, line->m_time - offset);
346 lastTime = line->m_time;
347 }
348 else
349 {
350 line->m_time = ++lastTime;
351 line->m_lyric = lyric;
352 }
353 }
354 }
355 else
356 {
357 // synthesize a time code from the track length and the number of lyrics lines
358 if (m_parent && !m_parent->isRadio())
359 {
360 line->m_time = std::chrono::milliseconds((m_parent->Length() / lyrics.count()) * x);
361 line->m_lyric = lyric;
362 lastTime = line->m_time;
363 }
364 else
365 {
366 line->m_time = ++lastTime;
367 line->m_lyric = lyric;
368 }
369 }
370
371 // ignore anything that is not a lyric
372 if (line->m_lyric.startsWith("[ti:") || line->m_lyric.startsWith("[al:") ||
373 line->m_lyric.startsWith("[ar:") || line->m_lyric.startsWith("[by:") ||
374 line->m_lyric.startsWith("[url:") || line->m_lyric.startsWith("[offset:") ||
375 line->m_lyric.startsWith("[id:") || line->m_lyric.startsWith("[length:") ||
376 line->m_lyric.startsWith("[au:") || line->m_lyric.startsWith("[la:"))
377 {
378 delete line;
379 continue;
380 }
381
382 m_lyricsMap.insert(line->m_time, line);
383 }
384}
QString artist(void)
Definition: lyricsdata.h:67
QString m_title
Definition: lyricsdata.h:118
~LyricsData() override
Definition: lyricsdata.cpp:22
bool m_changed
Definition: lyricsdata.h:120
void loadLyrics(const QString &xmlData)
Definition: lyricsdata.cpp:231
void findLyrics(const QString &grabber)
Definition: lyricsdata.cpp:50
LyricsLineMap * lyrics(void)
Definition: lyricsdata.h:76
Status m_status
Definition: lyricsdata.h:113
void customEvent(QEvent *event) override
Definition: lyricsdata.cpp:184
void save(void)
Definition: lyricsdata.cpp:117
QString grabber(void)
Definition: lyricsdata.h:64
QString album(void)
Definition: lyricsdata.h:70
QString createLyricsXML(void)
Definition: lyricsdata.cpp:138
MusicMetadata * m_parent
Definition: lyricsdata.h:111
void clearLyrics(void)
Definition: lyricsdata.cpp:38
bool m_syncronized
Definition: lyricsdata.h:119
void setLyrics(const QStringList &lyrics)
Definition: lyricsdata.cpp:313
void statusChanged(LyricsData::Status status, const QString &message)
bool syncronized(void) const
Definition: lyricsdata.h:79
QString title(void)
Definition: lyricsdata.h:73
@ STATUS_NOTFOUND
Definition: lyricsdata.h:90
@ STATUS_FOUND
Definition: lyricsdata.h:89
@ STATUS_NOTLOADED
Definition: lyricsdata.h:87
@ STATUS_SEARCHING
Definition: lyricsdata.h:88
QString m_album
Definition: lyricsdata.h:117
QString m_grabber
Definition: lyricsdata.h:115
void clear(void)
Definition: lyricsdata.cpp:27
LyricsLineMap m_lyricsMap
Definition: lyricsdata.h:109
QString m_artist
Definition: lyricsdata.h:116
QString toString(bool syncronized)
Definition: lyricsdata.h:29
bool isRadio(void) const
bool isDBTrack(void) const
QString Hostname(void)
std::chrono::milliseconds Length() const
QString Title() const
IdType ID() const
QString Artist() const
QString Album() const
bool SendReceiveStringList(QStringList &strlist, bool quickTimeout=false, bool block=true)
Send a message to the backend and wait for a response.
This class is used as a container for messages.
Definition: mythevent.h:17
const QString & Message() const
Definition: mythevent.h:65
static const Type kMythEventMessage
Definition: mythevent.h:79
void addListener(QObject *listener)
Add a listener to the observable.
void removeListener(QObject *listener)
Remove a listener to the observable.
unsigned int uint
Definition: freesurround.h:24
static const QRegularExpression kTimeCode
Definition: lyricsdata.cpp:17
static constexpr std::chrono::milliseconds millisecondsFromParts(int hours, int minutes=0, int seconds=0, int milliseconds=0)
Build a duration from separate minutes, seconds, etc.
Definition: mythchrono.h:109
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39