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