MythTV  master
lyricsdata.cpp
Go to the documentation of this file.
1 #include <iostream>
2 
3 #include "musicmetadata.h"
4 
5 // qt
6 #include <QRegExp>
7 #include <QDomDocument>
8 
9 // mythtv
10 #include "mythcontext.h"
11 
12 // libmythmetadata
13 #include "lyricsdata.h"
14 
15 
16 /*************************************************************************/
17 //LyricsData
18 
19 //LyricsData::LyricsData():
20 // m_parent(nullptr), m_status(STATUS_NOTLOADED), m_syncronized(false), m_changed(false) { }
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  QMap<int, LyricsLine*>::iterator 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  slist << artist
107  << album
108  << title;
109 
110  LOG(VB_NETWORK, LOG_INFO, QString("LyricsData:: Sending command %1").arg(slist.join('~')));
111 
113 }
114 
116 {
117  // only save the lyrics if they have been changed
118  if (!m_changed)
119  return;
120 
121  // only save lyrics if it is a DB track
122  if (!m_parent || !m_parent->isDBTrack())
123  return;
124 
125  // send a message to the master BE to save the lyrics for this track
126  QStringList slist;
127  slist << "MUSIC_LYRICS_SAVE"
128  << m_parent->Hostname()
129  << QString::number(m_parent->ID());
130 
131  slist << createLyricsXML();
132 
134 }
135 
137 {
138  QDomDocument doc("lyrics");
139 
140  QDomElement root = doc.createElement("lyrics");
141  doc.appendChild(root);
142 
143  // artist
144  QDomElement artist = doc.createElement("artist");
145  root.appendChild(artist);
146  artist.appendChild(doc.createTextNode(m_artist));
147 
148  // album
149  QDomElement album = doc.createElement("album");
150  root.appendChild(album);
151  album.appendChild(doc.createTextNode(m_album));
152 
153  // title
154  QDomElement title = doc.createElement("title");
155  root.appendChild(title);
156  title.appendChild(doc.createTextNode(m_title));
157 
158  // syncronized
159  QDomElement syncronized = doc.createElement("syncronized");
160  root.appendChild(syncronized);
161  syncronized.appendChild(doc.createTextNode(m_syncronized ? "True" : "False"));
162 
163  // grabber
164  QDomElement grabber = doc.createElement("grabber");
165  root.appendChild(grabber);
166  grabber.appendChild(doc.createTextNode(m_grabber));
167 
168  // lyrics
169  QMap<int, LyricsLine*>::iterator i = m_lyricsMap.begin();
170  while (i != m_lyricsMap.end())
171  {
172  LyricsLine *line = (*i);
173  QDomElement lyric = doc.createElement("lyric");
174  root.appendChild(lyric);
175  lyric.appendChild(doc.createTextNode(line->toString(m_syncronized)));
176  ++i;
177  }
178 
179  return doc.toString(4);
180 }
181 
182 void LyricsData::customEvent(QEvent *event)
183 {
184  if (event->type() == MythEvent::MythEventMessage)
185  {
186  auto *me = dynamic_cast<MythEvent*>(event);
187  if (!me)
188  return;
189 
190  // we are only interested in MUSIC_LYRICS_* messages
191  if (me->Message().startsWith("MUSIC_LYRICS_"))
192  {
193  QStringList list = me->Message().simplified().split(' ');
194 
195  if (list.size() >= 2)
196  {
197  uint songID = list[1].toUInt();
198 
199  // make sure the message is for us
200  if (m_parent->ID() == songID)
201  {
202  if (list[0] == "MUSIC_LYRICS_FOUND")
203  {
205 
206  QString xmlData = me->Message().section(" ", 2, -1);
207 
208  // we found some lyrics so load them
209  loadLyrics(xmlData);
210  emit statusChanged(m_status, "");
211  }
212  else if (list[0] == "MUSIC_LYRICS_STATUS")
213  {
214  emit statusChanged(STATUS_SEARCHING, me->Message().section(" ", 2, -1));
215  }
216  else
217  {
219  // nothing found or an error occured
221  emit statusChanged(m_status, tr("No lyrics found for this track"));
222  }
223  }
224  }
225  }
226  }
227 }
228 
229 void LyricsData::loadLyrics(const QString &xmlData)
230 {
231  QDomDocument domDoc;
232  QString errorMsg;
233  int errorLine = 0, errorColumn = 0;
234 
235  if (!domDoc.setContent(xmlData, false, &errorMsg, &errorLine, &errorColumn))
236  {
237  LOG(VB_GENERAL, LOG_ERR,
238  QString("LyricsData:: Could not parse lyrics from %1").arg(xmlData) +
239  QString("\n\t\t\tError at line: %1 column: %2 msg: %3").arg(errorLine).arg(errorColumn).arg(errorMsg));
241  return;
242  }
243 
244  QDomNodeList itemList = domDoc.elementsByTagName("lyrics");
245  QDomNode itemNode = itemList.item(0);
246 
247  m_grabber = itemNode.namedItem(QString("grabber")).toElement().text();
248  m_artist = itemNode.namedItem(QString("artist")).toElement().text();
249  m_album = itemNode.namedItem(QString("album")).toElement().text();
250  m_title = itemNode.namedItem(QString("title")).toElement().text();
251  m_syncronized = (itemNode.namedItem(QString("syncronized")).toElement().text() == "True");
252  m_changed = false;
253 
254  clearLyrics();
255 
256  itemList = itemNode.toElement().elementsByTagName("lyric");
257 
258  QStringList lyrics;
259 
260  for (int x = 0; x < itemList.count(); x++)
261  {
262  QDomNode lyricNode = itemList.at(x);
263  QString lyric = lyricNode.toElement().text();
264 
265  if (m_syncronized)
266  {
267  QStringList times;
268  int lastind = 0;
269  while (lyric.indexOf(QRegExp("\\[\\d\\d:\\d\\d\\]"),
270  lastind) == lastind ||
271  lyric.indexOf(QRegExp("\\[\\d\\d:\\d\\d\\.\\d\\d\\]"),
272  lastind) == lastind)
273  {
274  if (lyric[lastind+6] == '.')
275  {
276  times.append(lyric.mid(lastind,10));
277  lastind += 10;
278  }
279  else
280  {
281  // short version
282  times.append(lyric.mid(lastind,7));
283  lastind += 7;
284  }
285  }
286  if (!times.isEmpty())
287  {
288  for (int y = 0; y < times.count(); y++)
289  {
290  lyrics.append(times.at(y) + lyric.mid(lastind));
291  }
292  }
293  else
294  {
295  lyrics.append(lyric);
296  }
297  }
298  else
299  {
300  lyrics.append(lyric);
301  }
302  }
303 
304  setLyrics(lyrics);
305 
307 }
308 
309 void LyricsData::setLyrics(const QStringList &lyrics)
310 {
311  clearLyrics();
312 
313  int lastTime = -1;
314  int offset = 0; // offset in milliseconds
315 
316  for (int x = 0; x < lyrics.count(); x++)
317  {
318  QString lyric = lyrics.at(x);
319 
320  auto *line = new LyricsLine;
321 
322  if (lyric.startsWith("[offset:"))
323  {
324  offset = lyric.mid(8,lyric.indexOf("]", 8)-8).toInt();
325  }
326 
327  if (m_syncronized)
328  {
329  if (!lyric.isEmpty())
330  {
331  // does the line start with a time code like [12:34] or [12:34.56]
332  if (lyric.indexOf(QRegExp("\\[\\d\\d:\\d\\d\\]"), 0) == 0 ||
333  lyric.indexOf(QRegExp("\\[\\d\\d:\\d\\d\\.\\d\\d\\]"), 0) == 0)
334  {
335  int minutes = lyric.mid(1, 2).toInt();
336  int seconds = lyric.mid(4, 2).toInt();
337  int hundredths = 0;
338  if (lyric[6] == '.')
339  {
340  hundredths = lyric.mid(7, 2).toInt();
341  line->m_lyric = lyric.mid(10);
342  }
343  else
344  {
345  line->m_lyric = lyric.mid(7);
346  }
347  line->m_time = (minutes * 60 * 1000) + (seconds * 1000) + (hundredths * 10);
348  if (offset > 0)
349  {
350  if (offset > line->m_time) line->m_time = 0;
351  else line->m_time -= offset;
352  }
353  else
354  {
355  line->m_time -= offset;
356  }
357  lastTime = line->m_time;
358  }
359  else
360  {
361  line->m_time = ++lastTime;
362  line->m_lyric = lyric;
363  }
364  }
365  }
366  else
367  {
368  // synthesize a time code from the track length and the number of lyrics lines
369  if (m_parent && !m_parent->isRadio())
370  {
371  line->m_time = (m_parent->Length() / lyrics.count()) * x;
372  line->m_lyric = lyric;
373  lastTime = line->m_time;
374  }
375  else
376  {
377  line->m_time = ++lastTime;
378  line->m_lyric = lyric;
379  }
380  }
381 
382  // ignore anything that is not a lyric
383  if (line->m_lyric.startsWith("[ti:") || line->m_lyric.startsWith("[al:") ||
384  line->m_lyric.startsWith("[ar:") || line->m_lyric.startsWith("[by:") ||
385  line->m_lyric.startsWith("[url:") || line->m_lyric.startsWith("[offset:") ||
386  line->m_lyric.startsWith("[id:") || line->m_lyric.startsWith("[length:") ||
387  line->m_lyric.startsWith("[au:") || line->m_lyric.startsWith("[la:"))
388  {
389  delete line;
390  continue;
391  }
392 
393  m_lyricsMap.insert(line->m_time, line);
394  }
395 }
QString m_album
Definition: lyricsdata.h:115
QString title(void)
Definition: lyricsdata.h:71
void customEvent(QEvent *event) override
Definition: lyricsdata.cpp:182
bool isRadio(void) const
QMap< int, LyricsLine * > m_lyricsMap
Definition: lyricsdata.h:107
static Type MythEventMessage
Definition: mythevent.h:66
void removeListener(QObject *listener)
Remove a listener to the observable.
QString Hostname(void)
QString artist(void)
Definition: lyricsdata.h:65
int Length() const
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
void addListener(QObject *listener)
Add a listener to the observable.
QString grabber(void)
Definition: lyricsdata.h:62
void clearLyrics(void)
Definition: lyricsdata.cpp:38
QString toString(bool syncronized)
Definition: lyricsdata.h:27
void loadLyrics(const QString &xmlData)
Definition: lyricsdata.cpp:229
QString Artist() const
QString m_title
Definition: lyricsdata.h:116
bool isDBTrack(void) const
bool SendReceiveStringList(QStringList &strlist, bool quickTimeout=false, bool block=true)
Send a message to the backend and wait for a response.
void findLyrics(const QString &grabber)
Definition: lyricsdata.cpp:50
QString album(void)
Definition: lyricsdata.h:68
IdType ID() const
unsigned int uint
Definition: compat.h:140
bool syncronized(void)
Definition: lyricsdata.h:77
QString Album() const
QString m_grabber
Definition: lyricsdata.h:113
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
void clear(void)
Definition: lyricsdata.cpp:27
QString Title() const
void setLyrics(const QStringList &lyrics)
Definition: lyricsdata.cpp:309
Status m_status
Definition: lyricsdata.h:111
QMap< int, LyricsLine * > * lyrics(void)
Definition: lyricsdata.h:74
void save(void)
Definition: lyricsdata.cpp:115
bool m_syncronized
Definition: lyricsdata.h:117
QString m_artist
Definition: lyricsdata.h:114
QString createLyricsXML(void)
Definition: lyricsdata.cpp:136
bool m_changed
Definition: lyricsdata.h:118
void statusChanged(LyricsData::Status status, const QString &message)
MusicMetadata * m_parent
Definition: lyricsdata.h:109
QString m_lyric
Definition: lyricsdata.h:25