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