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  MythEvent *me = static_cast<MythEvent*>(event);
187 
188  if (!me)
189  return;
190 
191  // we are only interested in MUSIC_LYRICS_* messages
192  if (me->Message().startsWith("MUSIC_LYRICS_"))
193  {
194  QStringList list = me->Message().simplified().split(' ');
195 
196  if (list.size() >= 2)
197  {
198  uint songID = list[1].toUInt();
199 
200  // make sure the message is for us
201  if (m_parent->ID() == songID)
202  {
203  if (list[0] == "MUSIC_LYRICS_FOUND")
204  {
206 
207  QString xmlData = me->Message().section(" ", 2, -1);
208 
209  // we found some lyrics so load them
210  loadLyrics(xmlData);
211  emit statusChanged(m_status, "");
212  }
213  else if (list[0] == "MUSIC_LYRICS_STATUS")
214  {
215  emit statusChanged(STATUS_SEARCHING, me->Message().section(" ", 2, -1));
216  }
217  else
218  {
220  // nothing found or an error occured
222  emit statusChanged(m_status, tr("No lyrics found for this track"));
223  }
224  }
225  }
226  }
227  }
228 }
229 
230 void LyricsData::loadLyrics(const QString &xmlData)
231 {
232  QDomDocument domDoc;
233  QString errorMsg;
234  int errorLine, errorColumn;
235 
236  if (!domDoc.setContent(xmlData, false, &errorMsg, &errorLine, &errorColumn))
237  {
238  LOG(VB_GENERAL, LOG_ERR,
239  QString("LyricsData:: Could not parse lyrics from %1").arg(xmlData) +
240  QString("\n\t\t\tError at line: %1 column: %2 msg: %3").arg(errorLine).arg(errorColumn).arg(errorMsg));
242  return;
243  }
244 
245  QDomNodeList itemList = domDoc.elementsByTagName("lyrics");
246  QDomNode itemNode = itemList.item(0);
247 
248  m_grabber = itemNode.namedItem(QString("grabber")).toElement().text();
249  m_artist = itemNode.namedItem(QString("artist")).toElement().text();
250  m_album = itemNode.namedItem(QString("album")).toElement().text();
251  m_title = itemNode.namedItem(QString("title")).toElement().text();
252  m_syncronized = (itemNode.namedItem(QString("syncronized")).toElement().text() == "True");
253  m_changed = false;
254 
255  clearLyrics();
256 
257  itemList = itemNode.toElement().elementsByTagName("lyric");
258 
259  QStringList lyrics;
260 
261  for (int x = 0; x < itemList.count(); x++)
262  {
263  QDomNode lyricNode = itemList.at(x);
264  QString lyric = lyricNode.toElement().text();
265 
266  if (m_syncronized)
267  {
268  QStringList times;
269  int lastind = 0;
270  while (lyric.indexOf(QRegExp("\\[\\d\\d:\\d\\d\\]"),
271  lastind) == lastind ||
272  lyric.indexOf(QRegExp("\\[\\d\\d:\\d\\d\\.\\d\\d\\]"),
273  lastind) == lastind)
274  {
275  if (lyric[lastind+6] == '.')
276  {
277  times.append(lyric.mid(lastind,10));
278  lastind += 10;
279  }
280  else
281  {
282  // short version
283  times.append(lyric.mid(lastind,7));
284  lastind += 7;
285  }
286  }
287  if (!times.isEmpty())
288  {
289  for (int y = 0; y < times.count(); y++)
290  {
291  lyrics.append(times.at(y) + lyric.mid(lastind));
292  }
293  }
294  else
295  {
296  lyrics.append(lyric);
297  }
298  }
299  else
300  {
301  lyrics.append(lyric);
302  }
303  }
304 
305  setLyrics(lyrics);
306 
308 }
309 
310 void LyricsData::setLyrics(const QStringList &lyrics)
311 {
312  clearLyrics();
313 
314  int lastTime = -1;
315  int offset = 0; // offset in milliseconds
316 
317  for (int x = 0; x < lyrics.count(); x++)
318  {
319  QString lyric = lyrics.at(x);
320 
321  LyricsLine *line = new LyricsLine;
322 
323  if (lyric.startsWith("[offset:"))
324  {
325  offset = lyric.mid(8,lyric.indexOf("]", 8)-8).toInt();
326  }
327 
328  if (m_syncronized)
329  {
330  if (!lyric.isEmpty())
331  {
332  // does the line start with a time code like [12:34] or [12:34.56]
333  if (lyric.indexOf(QRegExp("\\[\\d\\d:\\d\\d\\]"), 0) == 0 ||
334  lyric.indexOf(QRegExp("\\[\\d\\d:\\d\\d\\.\\d\\d\\]"), 0) == 0)
335  {
336  int minutes = lyric.mid(1, 2).toInt();
337  int seconds = lyric.mid(4, 2).toInt();
338  int hundredths = 0;
339  if (lyric[6] == '.')
340  {
341  hundredths = lyric.mid(7, 2).toInt();
342  line->m_lyric = lyric.mid(10);
343  }
344  else
345  {
346  line->m_lyric = lyric.mid(7);
347  }
348  line->m_time = (minutes * 60 * 1000) + (seconds * 1000) + (hundredths * 10);
349  if (offset > 0)
350  {
351  if (offset > line->m_time) line->m_time = 0;
352  else line->m_time -= offset;
353  }
354  else
355  {
356  line->m_time -= offset;
357  }
358  lastTime = line->m_time;
359  }
360  else
361  {
362  line->m_time = ++lastTime;
363  line->m_lyric = lyric;
364  }
365  }
366  }
367  else
368  {
369  // synthesize a time code from the track length and the number of lyrics lines
370  if (m_parent && !m_parent->isRadio())
371  {
372  line->m_time = (m_parent->Length() / lyrics.count()) * x;
373  line->m_lyric = lyric;
374  lastTime = line->m_time;
375  }
376  else
377  {
378  line->m_time = ++lastTime;
379  line->m_lyric = lyric;
380  }
381  }
382 
383  // ignore anything that is not a lyric
384  if (line->m_lyric.startsWith("[ti:") || line->m_lyric.startsWith("[al:") ||
385  line->m_lyric.startsWith("[ar:") || line->m_lyric.startsWith("[by:") ||
386  line->m_lyric.startsWith("[url:") || line->m_lyric.startsWith("[offset:") ||
387  line->m_lyric.startsWith("[id:") || line->m_lyric.startsWith("[length:") ||
388  line->m_lyric.startsWith("[au:") || line->m_lyric.startsWith("[la:"))
389  {
390  delete line;
391  continue;
392  }
393 
394  m_lyricsMap.insert(line->m_time, line);
395  }
396 }
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
unsigned int uint
Definition: compat.h:140
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:230
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
This class is used as a container for messages.
Definition: mythevent.h:16
QString album(void)
Definition: lyricsdata.h:68
int m_time
Definition: lyricsdata.h:24
IdType ID() const
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:310
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
const QString & Message() const
Definition: mythevent.h:58
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