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