MythTV  master
musicmetautils.cpp
Go to the documentation of this file.
1 // qt
2 #include <QDir>
3 #include <QProcess>
4 #include <QDomDocument>
5 
6 // libmyth* headers
7 #include "mythconfig.h"
8 #include "exitcodes.h"
9 #include "mythlogging.h"
10 #include "storagegroup.h"
11 #include "musicmetadata.h"
12 #include "metaio.h"
13 #include "mythcontext.h"
14 #include "musicfilescanner.h"
15 #include "musicutils.h"
16 #include "mythdirs.h"
17 
18 extern "C" {
19 #include <libavformat/avformat.h>
20 #include <libavcodec/avcodec.h>
21 }
22 
23 // mythutils headers
24 #include "commandlineparser.h"
25 #include "musicmetautils.h"
26 
28 {
29  bool ok = true;
30  int result = GENERIC_EXIT_OK;
31 
32  if (cmdline.toString("songid").isEmpty())
33  {
34  LOG(VB_GENERAL, LOG_ERR, "Missing --songid option");
36  }
37  int songID = cmdline.toInt("songid");
38 
40  if (!mdata)
41  {
42  LOG(VB_GENERAL, LOG_ERR, QString("Cannot find metadata for trackid: %1").arg(songID));
43  return GENERIC_EXIT_NOT_OK;
44  }
45 
46  if (!cmdline.toString("title").isEmpty())
47  mdata->setTitle(cmdline.toString("title"));
48 
49  if (!cmdline.toString("artist").isEmpty())
50  mdata->setArtist(cmdline.toString("artist"));
51 
52  if (!cmdline.toString("album").isEmpty())
53  mdata->setAlbum(cmdline.toString("album"));
54 
55  if (!cmdline.toString("genre").isEmpty())
56  mdata->setGenre(cmdline.toString("genre"));
57 
58  if (!cmdline.toString("trackno").isEmpty())
59  mdata->setTrack(cmdline.toInt("trackno"));
60 
61  if (!cmdline.toString("year").isEmpty())
62  mdata->setYear(cmdline.toInt("year"));
63 
64  if (!cmdline.toString("rating").isEmpty())
65  mdata->setRating(cmdline.toInt("rating"));
66 
67  if (!cmdline.toString("playcount").isEmpty())
68  mdata->setPlaycount(cmdline.toInt("playcount"));
69 
70  if (!cmdline.toString("lastplayed").isEmpty())
71  mdata->setLastPlay(cmdline.toDateTime("lastplayed"));
72 
73  mdata->dumpToDatabase();
74 
75  MetaIO *tagger = mdata->getTagger();
76  if (tagger)
77  {
78  ok = tagger->write(mdata->getLocalFilename(), mdata);
79 
80  if (!ok)
81  LOG(VB_GENERAL, LOG_ERR, QString("Failed to write to tag for trackid: %1").arg(songID));
82  }
83 
84  // tell any clients that the metadata for this track has changed
85  gCoreContext->SendMessage(QString("MUSIC_METADATA_CHANGED %1").arg(songID));
86 
87  if (!ok)
88  result = GENERIC_EXIT_NOT_OK;
89 
90  return result;
91 }
92 
94 {
95  if (cmdline.toString("songid").isEmpty())
96  {
97  LOG(VB_GENERAL, LOG_ERR, "Missing --songid option");
99  }
100 
101  if (cmdline.toString("imagetype").isEmpty())
102  {
103  LOG(VB_GENERAL, LOG_ERR, "Missing --imagetype option");
105  }
106 
107  int songID = cmdline.toInt("songid");
108  ImageType type = (ImageType)cmdline.toInt("imagetype");
109 
111  if (!mdata)
112  {
113  LOG(VB_GENERAL, LOG_ERR, QString("Cannot find metadata for trackid: %1").arg(songID));
114  return GENERIC_EXIT_NOT_OK;
115  }
116 
117  AlbumArtImage *image = mdata->getAlbumArtImages()->getImage(type);
118  if (!image)
119  {
120  LOG(VB_GENERAL, LOG_ERR, QString("Cannot find image of type: %1").arg(type));
121  return GENERIC_EXIT_NOT_OK;
122  }
123 
124  MetaIO *tagger = mdata->getTagger();
125  if (!tagger)
126  {
127  LOG(VB_GENERAL, LOG_ERR, QString("Cannot find a tagger for this file: %1").arg(mdata->Filename(false)));
128  return GENERIC_EXIT_NOT_OK;
129  }
130 
131 
132  if (!image->m_embedded || !tagger->supportsEmbeddedImages())
133  {
134  LOG(VB_GENERAL, LOG_ERR, QString("Either the image isn't embedded or the tagger doesn't support embedded images"));
135  return GENERIC_EXIT_NOT_OK;
136  }
137 
138  // find the tracks actual filename
139  StorageGroup musicGroup("Music", gCoreContext->GetHostName(), false);
140  QString trackFilename = musicGroup.FindFile(mdata->Filename(false));
141 
142  // where are we going to save the image
143  QString path;
144  StorageGroup artGroup("MusicArt", gCoreContext->GetHostName(), false);
145  QStringList dirList = artGroup.GetDirList();
146  if (!dirList.empty())
147  path = artGroup.FindNextDirMostFree();
148 
149  if (!QDir(path).exists())
150  {
151  LOG(VB_GENERAL, LOG_ERR, "Cannot find a directory in the 'MusicArt' storage group to save to");
152  return GENERIC_EXIT_NOT_OK;
153  }
154 
155  path += "/AlbumArt/";
156  QDir dir(path);
157 
158  QString filename = QString("%1-%2.jpg").arg(mdata->ID()).arg(AlbumArtImages::getTypeFilename(image->m_imageType));
159 
160  if (QFile::exists(path + filename))
161  QFile::remove(path + filename);
162 
163  if (!dir.exists())
164  dir.mkpath(path);
165 
166  QImage *saveImage = tagger->getAlbumArt(trackFilename, image->m_imageType);
167  if (saveImage)
168  {
169  saveImage->save(path + filename, "JPEG");
170  delete saveImage;
171  }
172 
173  delete tagger;
174 
175  // tell any clients that the albumart for this track has changed
176  gCoreContext->SendMessage(QString("MUSIC_ALBUMART_CHANGED %1 %2").arg(songID).arg(type));
177 
178  return GENERIC_EXIT_OK;
179 }
180 
181 static int ScanMusic(const MythUtilCommandLineParser &/*cmdline*/)
182 {
183  auto *fscan = new MusicFileScanner();
184  QStringList dirList;
185 
187  {
188  LOG(VB_GENERAL, LOG_ERR, "Failed to find any directories in the 'Music' storage group");
189  delete fscan;
190  return GENERIC_EXIT_NOT_OK;
191  }
192 
193  fscan->SearchDirs(dirList);
194  delete fscan;
195 
196  return GENERIC_EXIT_OK;
197 }
198 
199 static int UpdateRadioStreams(const MythUtilCommandLineParser &/*cmdline*/)
200 {
201  // check we have the correct Music Schema Version (maybe the FE hasn't been run yet)
202  if (gCoreContext->GetNumSetting("MusicDBSchemaVer", 0) < 1024)
203  {
204  LOG(VB_GENERAL, LOG_ERR, "Can't update the radio streams the DB schema hasn't been updated yet! Aborting");
205  return GENERIC_EXIT_NOT_OK;
206  }
207 
209  return GENERIC_EXIT_NOT_OK;
210 
211  return GENERIC_EXIT_OK;
212 }
213 
215 {
216  if (cmdline.toString("songid").isEmpty())
217  {
218  LOG(VB_GENERAL, LOG_ERR, "Missing --songid option");
220  }
221 
222  int songID = cmdline.toInt("songid");
223 
225  if (!mdata)
226  {
227  LOG(VB_GENERAL, LOG_ERR, QString("Cannot find metadata for trackid: %1").arg(songID));
228  return GENERIC_EXIT_NOT_OK;
229  }
230 
231  QString musicFile = mdata->getLocalFilename();
232 
233  if (musicFile.isEmpty() || !QFile::exists(musicFile))
234  {
235  LOG(VB_GENERAL, LOG_ERR, QString("Cannot find file for trackid: %1").arg(songID));
236  return GENERIC_EXIT_NOT_OK;
237  }
238 
239  AVFormatContext *inputFC = nullptr;
240  AVInputFormat *fmt = nullptr;
241 
242  // Open track
243  LOG(VB_GENERAL, LOG_DEBUG, QString("CalcTrackLength: Opening '%1'")
244  .arg(musicFile));
245 
246  QByteArray inFileBA = musicFile.toLocal8Bit();
247 
248  int ret = avformat_open_input(&inputFC, inFileBA.constData(), fmt, nullptr);
249 
250  if (ret)
251  {
252  LOG(VB_GENERAL, LOG_ERR, "CalcTrackLength: Couldn't open input file" +
253  ENO);
254  return GENERIC_EXIT_NOT_OK;
255  }
256 
257  // Getting stream information
258  ret = avformat_find_stream_info(inputFC, nullptr);
259 
260  if (ret < 0)
261  {
262  LOG(VB_GENERAL, LOG_ERR,
263  QString("CalcTrackLength: Couldn't get stream info, error #%1").arg(ret));
264  avformat_close_input(&inputFC);
265  inputFC = nullptr;
266  return GENERIC_EXIT_NOT_OK;;
267  }
268 
269  int duration = 0;
270  long long time = 0;
271 
272  for (uint i = 0; i < inputFC->nb_streams; i++)
273  {
274  AVStream *st = inputFC->streams[i];
275  char buf[256];
276 
277  const AVCodec *pCodec = avcodec_find_decoder(st->codecpar->codec_id);
278  if (!pCodec)
279  {
280  LOG(VB_GENERAL, LOG_WARNING,
281  QString("avcodec_find_decoder fail for %1").arg(st->codecpar->codec_id));
282  continue;
283  }
284  AVCodecContext *avctx = avcodec_alloc_context3(pCodec);
285  avcodec_parameters_to_context(avctx, st->codecpar);
286  av_codec_set_pkt_timebase(avctx, st->time_base);
287 
288  avcodec_string(buf, sizeof(buf), avctx, static_cast<int>(false));
289 
290  switch (inputFC->streams[i]->codecpar->codec_type)
291  {
292  case AVMEDIA_TYPE_AUDIO:
293  {
294  AVPacket pkt;
295  av_init_packet(&pkt);
296 
297  while (av_read_frame(inputFC, &pkt) >= 0)
298  {
299  if (pkt.stream_index == (int)i)
300  time = time + pkt.duration;
301 
302  av_packet_unref(&pkt);
303  }
304 
305  duration = time * av_q2d(inputFC->streams[i]->time_base);
306  break;
307  }
308 
309  default:
310  LOG(VB_GENERAL, LOG_ERR,
311  QString("Skipping unsupported codec %1 on stream %2")
312  .arg(inputFC->streams[i]->codecpar->codec_type).arg(i));
313  break;
314  }
315  avcodec_free_context(&avctx);
316  }
317 
318  // Close input file
319  avformat_close_input(&inputFC);
320  inputFC = nullptr;
321 
322  if (mdata->Length() / 1000 != duration)
323  {
324  LOG(VB_GENERAL, LOG_INFO, QString("The length of this track in the database was %1s "
325  "it is now %2s").arg(mdata->Length() / 1000).arg(duration));
326 
327  // update the track length in the database
328  mdata->setLength(duration * 1000);
329  mdata->dumpToDatabase();
330 
331  // tell any clients that the metadata for this track has changed
332  gCoreContext->SendMessage(QString("MUSIC_METADATA_CHANGED %1").arg(songID));
333  }
334  else
335  {
336  LOG(VB_GENERAL, LOG_INFO, QString("The length of this track is unchanged %1s")
337  .arg(mdata->Length() / 1000));
338  }
339 
340  return GENERIC_EXIT_OK;
341 }
342 
344 {
345 public:
346  QString m_name;
347  QString m_filename;
348  int m_priority {99};
349 };
350 
352 {
353  // make sure our lyrics cache directory exists
354  QString lyricsDir = GetConfDir() + "/MythMusic/Lyrics/";
355  QDir dir(lyricsDir);
356  if (!dir.exists())
357  dir.mkpath(lyricsDir);
358 
359  if (cmdline.toString("songid").isEmpty())
360  {
361  LOG(VB_GENERAL, LOG_ERR, "Missing --songid option");
363  }
364 
365  int songID = cmdline.toInt("songid");
366  QString grabberName = "ALL";
367  QString lyricsFile;
368  QString artist;
369  QString album;
370  QString title;
371  QString filename;
372 
373  if (!cmdline.toString("grabber").isEmpty())
374  grabberName = cmdline.toString("grabber");
375 
376  if (ID_TO_REPO(songID) == RT_Database)
377  {
379  if (!mdata)
380  {
381  LOG(VB_GENERAL, LOG_ERR, QString("Cannot find metadata for trackid: %1").arg(songID));
382  return GENERIC_EXIT_NOT_OK;
383  }
384 
385  QString musicFile = mdata->getLocalFilename();
386 
387  if (musicFile.isEmpty() || !QFile::exists(musicFile))
388  {
389  LOG(VB_GENERAL, LOG_ERR, QString("Cannot find file for trackid: %1").arg(songID));
390  //return GENERIC_EXIT_NOT_OK;
391  }
392 
393  // first check if we have already saved a lyrics file for this track
394  lyricsFile = GetConfDir() + QString("/MythMusic/Lyrics/%1.txt").arg(songID);
395  if (QFile::exists(lyricsFile))
396  {
397  // if the user specified a specific grabber assume they want to
398  // re-search for the lyrics using the given grabber
399  if (grabberName != "ALL")
400  QFile::remove(lyricsFile);
401  else
402  {
403  // load these lyrics to speed up future lookups
404  QFile file(QLatin1String(qPrintable(lyricsFile)));
405  QString lyrics;
406 
407  if (file.open(QIODevice::ReadOnly))
408  {
409  QTextStream stream(&file);
410 
411  while (!stream.atEnd())
412  {
413  lyrics.append(stream.readLine());
414  }
415 
416  file.close();
417  }
418 
419  // tell any clients that a lyrics file is available for this track
420  gCoreContext->SendMessage(QString("MUSIC_LYRICS_FOUND %1 %2").arg(songID).arg(lyrics));
421 
422  return GENERIC_EXIT_OK;
423  }
424  }
425 
426  artist = mdata->Artist();
427  album = mdata->Album();
428  title = mdata->Title();
429  filename = mdata->getLocalFilename();
430  }
431  else
432  {
433  // must be a CD or Radio Track
434  if (cmdline.toString("artist").isEmpty())
435  {
436  LOG(VB_GENERAL, LOG_ERR, "Missing --artist option");
438  }
439  artist = cmdline.toString("artist");
440 
441  if (cmdline.toString("album").isEmpty())
442  {
443  LOG(VB_GENERAL, LOG_ERR, "Missing --album option");
445  }
446  album = cmdline.toString("album");
447 
448  if (cmdline.toString("title").isEmpty())
449  {
450  LOG(VB_GENERAL, LOG_ERR, "Missing --title option");
452  }
453  title = cmdline.toString("title");
454  }
455 
456  // not found so try the grabbers
457  // first get a list of available grabbers
458  QString scriptDir = GetShareDir() + "metadata/Music/lyrics";
459  QDir d(scriptDir);
460 
461  if (!d.exists())
462  {
463  LOG(VB_GENERAL, LOG_ERR, QString("Cannot find lyric scripts directory: %1").arg(scriptDir));
464  gCoreContext->SendMessage(QString("MUSIC_LYRICS_ERROR NO_SCRIPTS_DIR"));
465  return GENERIC_EXIT_NOT_OK;
466  }
467 
468  d.setFilter(QDir::Files | QDir::NoDotAndDotDot);
469  d.setNameFilters(QStringList("*.py"));
470  QFileInfoList list = d.entryInfoList();
471  if (list.isEmpty())
472  {
473  LOG(VB_GENERAL, LOG_ERR, QString("Cannot find any lyric scripts in: %1").arg(scriptDir));
474  gCoreContext->SendMessage(QString("MUSIC_LYRICS_ERROR NO_SCRIPTS_FOUND"));
475  return GENERIC_EXIT_NOT_OK;
476  }
477 
478  QStringList scripts;
479  QFileInfoList::const_iterator it = list.begin();
480 
481  while (it != list.end())
482  {
483  const QFileInfo *fi = &(*it);
484  ++it;
485  LOG(VB_GENERAL, LOG_NOTICE, QString("Found lyric script at: %1").arg(fi->filePath()));
486  scripts.append(fi->filePath());
487  }
488 
489  QMap<int, LyricsGrabber> grabberMap;
490 
491  // query the grabbers to get their priority
492  for (int x = 0; x < scripts.count(); x++)
493  {
494  QProcess p;
495  p.start(QString("%1 %2 -v").arg(PYTHON_EXE).arg(scripts.at(x)));
496  p.waitForFinished(-1);
497  QString result = p.readAllStandardOutput();
498 
499  QDomDocument domDoc;
500  QString errorMsg;
501  int errorLine = 0;
502  int errorColumn = 0;
503 
504  if (!domDoc.setContent(result, false, &errorMsg, &errorLine, &errorColumn))
505  {
506  LOG(VB_GENERAL, LOG_ERR,
507  QString("FindLyrics: Could not parse version from %1").arg(scripts.at(x)) +
508  QString("\n\t\t\tError at line: %1 column: %2 msg: %3").arg(errorLine).arg(errorColumn).arg(errorMsg));
509  continue;
510  }
511 
512  QDomNodeList itemList = domDoc.elementsByTagName("grabber");
513  QDomNode itemNode = itemList.item(0);
514 
515  LyricsGrabber grabber;
516  grabber.m_name = itemNode.namedItem(QString("name")).toElement().text();
517  grabber.m_priority = itemNode.namedItem(QString("priority")).toElement().text().toInt();
518  grabber.m_filename = scripts.at(x);
519 
520  grabberMap.insert(grabber.m_priority, grabber);
521  }
522 
523  // try each grabber in turn until we find a match
524  QMap<int, LyricsGrabber>::const_iterator i = grabberMap.constBegin();
525  while (i != grabberMap.constEnd())
526  {
527  LyricsGrabber grabber = i.value();
528 
529  ++i;
530 
531  if (grabberName != "ALL" && grabberName != grabber.m_name)
532  continue;
533 
534  LOG(VB_GENERAL, LOG_NOTICE, QString("Trying grabber: %1, Priority: %2").arg(grabber.m_name).arg(grabber.m_priority));
535  QString statusMessage = QObject::tr("Searching '%1' for lyrics...").arg(grabber.m_name);
536  gCoreContext->SendMessage(QString("MUSIC_LYRICS_STATUS %1 %2").arg(songID).arg(statusMessage));
537 
538  QProcess p;
539  p.start(QString(R"(%1 %2 --artist="%3" --album="%4" --title="%5" --filename="%6")")
540  .arg(PYTHON_EXE).arg(grabber.m_filename).arg(artist).arg(album).arg(title).arg(filename));
541  p.waitForFinished(-1);
542  QString result = p.readAllStandardOutput();
543 
544  LOG(VB_GENERAL, LOG_DEBUG, QString("Grabber: %1, Exited with code: %2").arg(grabber.m_name).arg(p.exitCode()));
545 
546  if (p.exitCode() == 0)
547  {
548  LOG(VB_GENERAL, LOG_NOTICE, QString("Lyrics Found using: %1").arg(grabber.m_name));
549 
550  // save these lyrics to speed up future lookups if it is a DB track
551  if (ID_TO_REPO(songID) == RT_Database)
552  {
553  QFile file(QLatin1String(qPrintable(lyricsFile)));
554 
555  if (file.open(QIODevice::WriteOnly))
556  {
557  QTextStream stream(&file);
558  stream << result;
559  file.close();
560  }
561  }
562 
563  gCoreContext->SendMessage(QString("MUSIC_LYRICS_FOUND %1 %2").arg(songID).arg(result));
564  return GENERIC_EXIT_OK;
565  }
566  }
567 
568  // if we got here we didn't find any lyrics
569  gCoreContext->SendMessage(QString("MUSIC_LYRICS_NOTFOUND %1").arg(songID));
570 
571  return GENERIC_EXIT_OK;
572 }
573 
575 {
576  utilMap["updatemeta"] = &UpdateMeta;
577  utilMap["extractimage"] = &ExtractImage;
578  utilMap["scanmusic"] = &ScanMusic;
579  utilMap["updateradiostreams"] = &UpdateRadioStreams;
580  utilMap["calctracklen"] = &CalcTrackLength;
581  utilMap["findlyrics"] = &FindLyrics;
582 }
QDateTime toDateTime(const QString &key) const
Returns stored QVariant as a QDateTime, falling to default if not provided.
#define ID_TO_REPO(x)
Definition: musicmetadata.h:71
static MusicMetadata * createFromID(int trackid)
void setTrack(int ltrack)
#define GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:10
void setYear(int lyear)
virtual QImage * getAlbumArt(const QString &filename, ImageType type)
Definition: metaio.h:103
void setLength(int llength)
QString getLocalFilename(void)
try to find the track on the local file system
void registerMusicUtils(UtilMap &utilMap)
static QString getTypeFilename(ImageType type)
AlbumArtImages * getAlbumArtImages(void)
int Length() const
static int FindLyrics(const MythUtilCommandLineParser &cmdline)
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
void SendMessage(const QString &message)
ImageType m_imageType
Definition: musicmetadata.h:51
Definition: metaio.h:17
QString GetConfDir(void)
Definition: mythdirs.cpp:224
QString Artist() const
void dumpToDatabase(void)
QMap< QString, UtilFunc > UtilMap
Definition: mythutil.h:15
static bool updateStreamList(void)
static int UpdateMeta(const MythUtilCommandLineParser &cmdline)
virtual bool supportsEmbeddedImages(void)
Does the tag support embedded cover art.
Definition: metaio.h:60
static const uint16_t * d
QString toString(const QString &key) const
Returns stored QVariant as a QString, falling to default if not provided.
IdType ID() const
void setAlbum(const QString &lalbum, const QString &lalbum_sort=nullptr)
QString GetShareDir(void)
Definition: mythdirs.cpp:222
void setRating(int lrating)
unsigned int uint
Definition: compat.h:140
QString Album() const
MythCommFlagCommandLineParser cmdline
#define ENO
This can be appended to the LOG args with "+".
Definition: mythlogging.h:69
void setTitle(const QString &ltitle, const QString &ltitle_sort=nullptr)
void setArtist(const QString &lartist, const QString &lartist_sort=nullptr)
static int ExtractImage(const MythUtilCommandLineParser &cmdline)
MetaIO * getTagger(void)
static int CalcTrackLength(const MythUtilCommandLineParser &cmdline)
int GetNumSetting(const QString &key, int defaultval=0)
virtual bool write(const QString &filename, MusicMetadata *mdata)=0
Writes all metadata back to a file.
AlbumArtImage * getImage(ImageType type)
QString FindFile(const QString &filename)
QString Title() const
void setPlaycount(int lplaycount)
QString Filename(bool find=true)
static int ScanMusic(const MythUtilCommandLineParser &)
ImageType
Definition: musicmetadata.h:28
#define GENERIC_EXIT_INVALID_CMDLINE
Command line parse error.
Definition: exitcodes.h:15
#define GENERIC_EXIT_NOT_OK
Exited with error.
Definition: exitcodes.h:11
static bool FindDirs(const QString &group="Default", const QString &hostname="", QStringList *dirlist=nullptr)
Finds and and optionally initialize a directory list associated with a Storage Group.
QString GetHostName(void)
int toInt(const QString &key) const
Returns stored QVariant as an integer, falling to default if not provided.
static int UpdateRadioStreams(const MythUtilCommandLineParser &)
void setGenre(const QString &lgenre)
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:23