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  {
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  QMap<int, LyricsLine*>::iterator 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::MythEventMessage)
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  QString errorMsg;
235  int errorLine = 0;
236  int errorColumn = 0;
237 
238  if (!domDoc.setContent(xmlData, false, &errorMsg, &errorLine, &errorColumn))
239  {
240  LOG(VB_GENERAL, LOG_ERR,
241  QString("LyricsData:: Could not parse lyrics from %1").arg(xmlData) +
242  QString("\n\t\t\tError at line: %1 column: %2 msg: %3").arg(errorLine).arg(errorColumn).arg(errorMsg));
244  return;
245  }
246 
247  QDomNodeList itemList = domDoc.elementsByTagName("lyrics");
248  QDomNode itemNode = itemList.item(0);
249 
250  m_grabber = itemNode.namedItem(QString("grabber")).toElement().text();
251  m_artist = itemNode.namedItem(QString("artist")).toElement().text();
252  m_album = itemNode.namedItem(QString("album")).toElement().text();
253  m_title = itemNode.namedItem(QString("title")).toElement().text();
254  m_syncronized = (itemNode.namedItem(QString("syncronized")).toElement().text() == "True");
255  m_changed = false;
256 
257  clearLyrics();
258 
259  itemList = itemNode.toElement().elementsByTagName("lyric");
260 
261  QStringList lyrics;
262 
263  for (int x = 0; x < itemList.count(); x++)
264  {
265  QDomNode lyricNode = itemList.at(x);
266  QString lyric = lyricNode.toElement().text();
267 
268  if (m_syncronized)
269  {
270  QStringList times;
271  int lastind = 0;
272  while (lyric.indexOf(QRegExp(R"(\[\d\d:\d\d\])"),
273  lastind) == lastind ||
274  lyric.indexOf(QRegExp(R"(\[\d\d:\d\d\.\d\d\])"),
275  lastind) == lastind)
276  {
277  if (lyric[lastind+6] == '.')
278  {
279  times.append(lyric.mid(lastind,10));
280  lastind += 10;
281  }
282  else
283  {
284  // short version
285  times.append(lyric.mid(lastind,7));
286  lastind += 7;
287  }
288  }
289  if (!times.isEmpty())
290  {
291  for (int y = 0; y < times.count(); y++)
292  {
293  lyrics.append(times.at(y) + lyric.mid(lastind));
294  }
295  }
296  else
297  {
298  lyrics.append(lyric);
299  }
300  }
301  else
302  {
303  lyrics.append(lyric);
304  }
305  }
306 
307  setLyrics(lyrics);
308 
310 }
311 
312 void LyricsData::setLyrics(const QStringList &lyrics)
313 {
314  clearLyrics();
315 
316  int lastTime = -1;
317  int offset = 0; // offset in milliseconds
318 
319  for (int x = 0; x < lyrics.count(); x++)
320  {
321  QString lyric = lyrics.at(x);
322 
323  auto *line = new LyricsLine;
324 
325  if (lyric.startsWith("[offset:"))
326  {
327  offset = lyric.mid(8,lyric.indexOf("]", 8)-8).toInt();
328  }
329 
330  if (m_syncronized)
331  {
332  if (!lyric.isEmpty())
333  {
334  // does the line start with a time code like [12:34] or [12:34.56]
335  if (lyric.indexOf(QRegExp(R"(\[\d\d:\d\d\])"), 0) == 0 ||
336  lyric.indexOf(QRegExp(R"(\[\d\d:\d\d\.\d\d\])"), 0) == 0)
337  {
338  int minutes = lyric.mid(1, 2).toInt();
339  int seconds = lyric.mid(4, 2).toInt();
340  int hundredths = 0;
341  if (lyric[6] == '.')
342  {
343  hundredths = lyric.mid(7, 2).toInt();
344  line->m_lyric = lyric.mid(10);
345  }
346  else
347  {
348  line->m_lyric = lyric.mid(7);
349  }
350  line->m_time = (minutes * 60 * 1000) + (seconds * 1000) + (hundredths * 10);
351  if (offset > 0)
352  {
353  if (offset > line->m_time) line->m_time = 0;
354  else line->m_time -= offset;
355  }
356  else
357  {
358  line->m_time -= offset;
359  }
360  lastTime = line->m_time;
361  }
362  else
363  {
364  line->m_time = ++lastTime;
365  line->m_lyric = lyric;
366  }
367  }
368  }
369  else
370  {
371  // synthesize a time code from the track length and the number of lyrics lines
372  if (m_parent && !m_parent->isRadio())
373  {
374  line->m_time = (m_parent->Length() / lyrics.count()) * x;
375  line->m_lyric = lyric;
376  lastTime = line->m_time;
377  }
378  else
379  {
380  line->m_time = ++lastTime;
381  line->m_lyric = lyric;
382  }
383  }
384 
385  // ignore anything that is not a lyric
386  if (line->m_lyric.startsWith("[ti:") || line->m_lyric.startsWith("[al:") ||
387  line->m_lyric.startsWith("[ar:") || line->m_lyric.startsWith("[by:") ||
388  line->m_lyric.startsWith("[url:") || line->m_lyric.startsWith("[offset:") ||
389  line->m_lyric.startsWith("[id:") || line->m_lyric.startsWith("[length:") ||
390  line->m_lyric.startsWith("[au:") || line->m_lyric.startsWith("[la:"))
391  {
392  delete line;
393  continue;
394  }
395 
396  m_lyricsMap.insert(line->m_time, line);
397  }
398 }
LyricsData::STATUS_NOTLOADED
@ STATUS_NOTLOADED
Definition: lyricsdata.h:84
MusicMetadata::Title
QString Title() const
Definition: musicmetadata.h:162
MythEvent::MythEventMessage
static Type MythEventMessage
Definition: mythevent.h:73
root
QDomElement root
Definition: mythplugins/mytharchive/mytharchivehelper/main.cpp:653
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:1379
doc
QDomDocument doc("MYTHARCHIVEITEM")
LyricsData::m_grabber
QString m_grabber
Definition: lyricsdata.h:112
MythEvent
This class is used as a container for messages.
Definition: mythevent.h:17
MusicMetadata::isRadio
bool isRadio(void) const
Definition: musicmetadata.h:223
arg
arg(title).arg(filename).arg(doDelete))
MusicMetadata::ID
IdType ID() const
Definition: musicmetadata.h:217
LyricsData::m_status
Status m_status
Definition: lyricsdata.h:110
LyricsData::clear
void clear(void)
Definition: lyricsdata.cpp:27
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:23
MusicMetadata::Artist
QString Artist() const
Definition: musicmetadata.h:126
MythEvent::Message
const QString & Message() const
Definition: mythevent.h:65
LyricsData::setLyrics
void setLyrics(const QStringList &lyrics)
Definition: lyricsdata.cpp:312
LyricsData::m_artist
QString m_artist
Definition: lyricsdata.h:113
LyricsData::lyrics
QMap< int, LyricsLine * > * lyrics(void)
Definition: lyricsdata.h:73
LyricsData::STATUS_FOUND
@ STATUS_FOUND
Definition: lyricsdata.h:86
LyricsData::m_changed
bool m_changed
Definition: lyricsdata.h:117
MythObservable::addListener
void addListener(QObject *listener)
Add a listener to the observable.
Definition: mythobservable.cpp:38
LyricsData::STATUS_SEARCHING
@ STATUS_SEARCHING
Definition: lyricsdata.h:85
LyricsData::save
void save(void)
Definition: lyricsdata.cpp:117
LyricsLine::m_lyric
QString m_lyric
Definition: lyricsdata.h:27
LyricsData::statusChanged
void statusChanged(LyricsData::Status status, const QString &message)
LyricsData::m_syncronized
bool m_syncronized
Definition: lyricsdata.h:116
LyricsData::createLyricsXML
QString createLyricsXML(void)
Definition: lyricsdata.cpp:138
LyricsLine
Definition: lyricsdata.h:20
LyricsData::STATUS_NOTFOUND
@ STATUS_NOTFOUND
Definition: lyricsdata.h:87
LyricsData::m_parent
MusicMetadata * m_parent
Definition: lyricsdata.h:108
MusicMetadata::isDBTrack
bool isDBTrack(void) const
Definition: musicmetadata.h:222
LyricsData::customEvent
void customEvent(QEvent *event) override
Definition: lyricsdata.cpp:184
LyricsData::m_lyricsMap
QMap< int, LyricsLine * > m_lyricsMap
Definition: lyricsdata.h:106
LyricsData::m_album
QString m_album
Definition: lyricsdata.h:114
uint
unsigned int uint
Definition: compat.h:140
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:76
LyricsData::artist
QString artist(void)
Definition: lyricsdata.h:64
MusicMetadata::Album
QString Album() const
Definition: musicmetadata.h:150
LyricsData::title
QString title(void)
Definition: lyricsdata.h:70
LyricsData::~LyricsData
~LyricsData() override
Definition: lyricsdata.cpp:22
MusicMetadata::Hostname
QString Hostname(void)
Definition: musicmetadata.h:229
LyricsData::grabber
QString grabber(void)
Definition: lyricsdata.h:61
mythcontext.h
LyricsData::clearLyrics
void clearLyrics(void)
Definition: lyricsdata.cpp:38
LyricsData::m_title
QString m_title
Definition: lyricsdata.h:115
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:67
LyricsData::loadLyrics
void loadLyrics(const QString &xmlData)
Definition: lyricsdata.cpp:231
MusicMetadata::Length
int Length() const
Definition: musicmetadata.h:205
MythObservable::removeListener
void removeListener(QObject *listener)
Remove a listener to the observable.
Definition: mythobservable.cpp:55
musicmetadata.h