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