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
10 #include "libmythbase/mythchrono.h"
13 
14 // libmythmetadata
15 #include "lyricsdata.h"
16 
17 static const QRegularExpression kTimeCode { R"(^(\[(\d\d):(\d\d)(?:\.(\d\d))?\])(.*))" };
18 
19 /*************************************************************************/
20 //LyricsData
21 
23 {
24  clear();
25 }
26 
28 {
29  m_grabber = m_artist = m_album = m_title = "";
30 
31  clearLyrics();
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 
50 void LyricsData::findLyrics(const QString &grabber)
51 {
52  if (!m_parent)
53  return;
54 
55  switch (m_status)
56  {
57  case STATUS_SEARCHING:
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 
184 void 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 
231 void 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 
308  setLyrics(lyrics);
309 
311 }
312 
313 void 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 }
MusicMetadata::Title
QString Title() const
Definition: musicmetadata.h:161
MythEvent::kMythEventMessage
static const Type kMythEventMessage
Definition: mythevent.h:79
MythCoreContext::SendReceiveStringList
bool SendReceiveStringList(QStringList &strlist, bool quickTimeout=false, bool block=true)
Send a message to the backend and wait for a response.
Definition: mythcorecontext.cpp:1381
LyricsData::lyrics
LyricsLineMap * lyrics(void)
Definition: lyricsdata.h:76
LyricsData::m_grabber
QString m_grabber
Definition: lyricsdata.h:115
LyricsData::STATUS_NOTFOUND
@ STATUS_NOTFOUND
Definition: lyricsdata.h:90
LyricsData::STATUS_SEARCHING
@ STATUS_SEARCHING
Definition: lyricsdata.h:88
MythEvent
This class is used as a container for messages.
Definition: mythevent.h:16
MusicMetadata::isRadio
bool isRadio(void) const
Definition: musicmetadata.h:223
MusicMetadata::ID
IdType ID() const
Definition: musicmetadata.h:217
LyricsData::m_status
Status m_status
Definition: lyricsdata.h:113
LyricsData::clear
void clear(void)
Definition: lyricsdata.cpp:27
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MusicMetadata::Length
std::chrono::milliseconds Length() const
Definition: musicmetadata.h:204
MusicMetadata::Artist
QString Artist() const
Definition: musicmetadata.h:125
kTimeCode
static const QRegularExpression kTimeCode
Definition: lyricsdata.cpp:17
millisecondsFromParts
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
MythEvent::Message
const QString & Message() const
Definition: mythevent.h:65
LyricsData::setLyrics
void setLyrics(const QStringList &lyrics)
Definition: lyricsdata.cpp:313
LyricsData::m_artist
QString m_artist
Definition: lyricsdata.h:116
LyricsData::m_changed
bool m_changed
Definition: lyricsdata.h:120
MythObservable::addListener
void addListener(QObject *listener)
Add a listener to the observable.
Definition: mythobservable.cpp:38
LyricsData::save
void save(void)
Definition: lyricsdata.cpp:117
mythlogging.h
LyricsData::statusChanged
void statusChanged(LyricsData::Status status, const QString &message)
LyricsData::m_syncronized
bool m_syncronized
Definition: lyricsdata.h:119
LyricsData::createLyricsXML
QString createLyricsXML(void)
Definition: lyricsdata.cpp:138
LyricsLine
Definition: lyricsdata.h:19
LyricsData::m_parent
MusicMetadata * m_parent
Definition: lyricsdata.h:111
MusicMetadata::isDBTrack
bool isDBTrack(void) const
Definition: musicmetadata.h:222
LyricsData::STATUS_FOUND
@ STATUS_FOUND
Definition: lyricsdata.h:89
LyricsData::customEvent
void customEvent(QEvent *event) override
Definition: lyricsdata.cpp:184
LyricsData::m_album
QString m_album
Definition: lyricsdata.h:117
gCoreContext
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
Definition: mythcorecontext.cpp:57
LyricsData::syncronized
bool syncronized(void) const
Definition: lyricsdata.h:79
LyricsData::STATUS_NOTLOADED
@ STATUS_NOTLOADED
Definition: lyricsdata.h:87
LyricsData::artist
QString artist(void)
Definition: lyricsdata.h:67
MusicMetadata::Album
QString Album() const
Definition: musicmetadata.h:149
LyricsData::title
QString title(void)
Definition: lyricsdata.h:73
LyricsData::~LyricsData
~LyricsData() override
Definition: lyricsdata.cpp:22
LyricsData::m_lyricsMap
LyricsLineMap m_lyricsMap
Definition: lyricsdata.h:109
MusicMetadata::Hostname
QString Hostname(void)
Definition: musicmetadata.h:229
mythcorecontext.h
LyricsData::grabber
QString grabber(void)
Definition: lyricsdata.h:64
mythchrono.h
LyricsData::clearLyrics
void clearLyrics(void)
Definition: lyricsdata.cpp:38
LyricsData::m_title
QString m_title
Definition: lyricsdata.h:118
LyricsLine::toString
QString toString(bool syncronized)
Definition: lyricsdata.h:29
LyricsData::findLyrics
void findLyrics(const QString &grabber)
Definition: lyricsdata.cpp:50
lyricsdata.h
LyricsData::album
QString album(void)
Definition: lyricsdata.h:70
LyricsData::loadLyrics
void loadLyrics(const QString &xmlData)
Definition: lyricsdata.cpp:231
MythObservable::removeListener
void removeListener(QObject *listener)
Remove a listener to the observable.
Definition: mythobservable.cpp:55
musicmetadata.h
uint
unsigned int uint
Definition: freesurround.h:24