Re-wrote MediaInfo output parsing code. Now using XML-based output.
[LameXP.git] / src / Thread_FileAnalyzer_Task.cpp
blob05becab81f1518edab03be471f164e137b9328bd
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2017 LoRd_MuldeR <MuldeR2@GMX.de>
4 //
5 // This program is free software; you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation; either version 2 of the License, or
8 // (at your option) any later version, but always including the *additional*
9 // restrictions defined in the "License.txt" file.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License along
17 // with this program; if not, write to the Free Software Foundation, Inc.,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 // http://www.gnu.org/licenses/gpl-2.0.txt
21 ///////////////////////////////////////////////////////////////////////////////
23 #include "Thread_FileAnalyzer_Task.h"
25 //Internal
26 #include "Global.h"
27 #include "LockedFile.h"
28 #include "Model_AudioFile.h"
29 #include "MimeTypes.h"
31 //MUtils
32 #include <MUtils/Global.h>
33 #include <MUtils/OSSupport.h>
34 #include <MUtils/Exception.h>
36 //Qt
37 #include <QDir>
38 #include <QFileInfo>
39 #include <QProcess>
40 #include <QDate>
41 #include <QTime>
42 #include <QDebug>
43 #include <QImage>
44 #include <QReadLocker>
45 #include <QWriteLocker>
46 #include <QThread>
47 #include <QXmlSimpleReader>
48 #include <QXmlInputSource>
49 #include <QStack>
51 //CRT
52 #include <math.h>
53 #include <time.h>
54 #include <assert.h>
56 #define IS_KEY(KEY) (key.compare(KEY, Qt::CaseInsensitive) == 0)
57 #define IS_SEC(SEC) (key.startsWith((SEC "_"), Qt::CaseInsensitive))
58 #define FIRST_TOK(STR) (STR.split(" ", QString::SkipEmptyParts).first())
60 #define STR_EQ(A,B) ((A).compare((B), Qt::CaseInsensitive) == 0)
62 ////////////////////////////////////////////////////////////
63 // XML Content Handler
64 ////////////////////////////////////////////////////////////
66 class AnalyzeTask_XmlHandler : public QXmlDefaultHandler
68 public:
69 AnalyzeTask_XmlHandler(AudioFileModel &audioFile) :
70 m_audioFile(audioFile), m_trackType(trackType_non), m_trackIdx(0), m_properties(initializeProperties()) {}
72 protected:
73 virtual bool startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &atts);
74 virtual bool endElement(const QString &namespaceURI, const QString &localName, const QString &qName);
75 virtual bool characters(const QString& ch);
77 private:
78 typedef enum
80 trackType_non = 0,
81 trackType_gen = 1,
82 trackType_aud = 2,
84 trackType_t;
86 typedef enum
88 propertyId_gen_format,
89 propertyId_gen_format_profile,
90 propertyId_gen_duration,
91 propertyId_aud_format,
92 propertyId_aud_format_version,
93 propertyId_aud_format_profile,
94 propertyId_aud_channel_s_,
95 propertyId_aud_samplingrate
97 propertyId_t;
99 const QMap<QPair<trackType_t, QString>, propertyId_t> &m_properties;
101 QStack<QString> m_stack;
102 AudioFileModel &m_audioFile;
103 trackType_t m_trackType;
104 quint32 m_trackIdx;
105 QPair<trackType_t, QString> m_currentProperty;
107 static QReadWriteLock s_propertiesMutex;
108 static QScopedPointer<const QMap<QPair<trackType_t, QString>, propertyId_t>> s_propertiesMap;
110 bool updatePropertry(const QPair<trackType_t, QString> &id, const QString &value);
112 static const QMap<QPair<trackType_t, QString>, propertyId_t> &initializeProperties();
113 static bool parseUnsigned(const QString &str, quint32 &value);
114 static quint32 decodeTime(quint32 &value);
117 ////////////////////////////////////////////////////////////
118 // Constructor
119 ////////////////////////////////////////////////////////////
121 AnalyzeTask::AnalyzeTask(const int taskId, const QString &inputFile, QAtomicInt &abortFlag)
123 m_taskId(taskId),
124 m_inputFile(inputFile),
125 m_mediaInfoBin(lamexp_tools_lookup("mediainfo.exe")),
126 m_avs2wavBin(lamexp_tools_lookup("avs2wav.exe")),
127 m_abortFlag(abortFlag)
129 if(m_mediaInfoBin.isEmpty() || m_avs2wavBin.isEmpty())
131 qFatal("Invalid path to MediaInfo binary. Tool not initialized properly.");
135 AnalyzeTask::~AnalyzeTask(void)
137 emit taskCompleted(m_taskId);
140 ////////////////////////////////////////////////////////////
141 // Thread Main
142 ////////////////////////////////////////////////////////////
144 void AnalyzeTask::run()
148 run_ex();
150 catch(const std::exception &error)
152 MUTILS_PRINT_ERROR("\nGURU MEDITATION !!!\n\nException error:\n%s\n", error.what());
153 MUtils::OS::fatal_exit(L"Unhandeled C++ exception error, application will exit!");
155 catch(...)
157 MUTILS_PRINT_ERROR("\nGURU MEDITATION !!!\n\nUnknown exception error!\n");
158 MUtils::OS::fatal_exit(L"Unhandeled C++ exception error, application will exit!");
162 void AnalyzeTask::run_ex(void)
164 int fileType = fileTypeNormal;
165 QString currentFile = QDir::fromNativeSeparators(m_inputFile);
166 qDebug("Analyzing: %s", MUTILS_UTF8(currentFile));
168 AudioFileModel fileInfo(currentFile);
169 analyzeFile(currentFile, fileInfo, &fileType);
171 if(MUTILS_BOOLIFY(m_abortFlag))
173 qWarning("Operation cancelled by user!");
174 return;
177 switch(fileType)
179 case fileTypeDenied:
180 qWarning("Cannot access file for reading, skipping!");
181 break;
182 case fileTypeCDDA:
183 qWarning("Dummy CDDA file detected, skipping!");
184 break;
185 default:
186 if(fileInfo.metaInfo().title().isEmpty() || fileInfo.techInfo().containerType().isEmpty() || fileInfo.techInfo().audioType().isEmpty())
188 fileType = fileTypeUnknown;
189 if(!QFileInfo(currentFile).suffix().compare("cue", Qt::CaseInsensitive))
191 qWarning("Cue Sheet file detected, skipping!");
192 fileType = fileTypeCueSheet;
194 else if(!QFileInfo(currentFile).suffix().compare("avs", Qt::CaseInsensitive))
196 qDebug("Found a potential Avisynth script, investigating...");
197 if(analyzeAvisynthFile(currentFile, fileInfo))
199 fileType = fileTypeNormal;
201 else
203 qDebug("Rejected Avisynth file: %s", MUTILS_UTF8(fileInfo.filePath()));
206 else
208 qDebug("Rejected file of unknown type: %s", MUTILS_UTF8(fileInfo.filePath()));
211 break;
214 //Emit the file now!
215 emit fileAnalyzed(m_taskId, fileType, fileInfo);
218 ////////////////////////////////////////////////////////////
219 // Privtae Functions
220 ////////////////////////////////////////////////////////////
222 const AudioFileModel& AnalyzeTask::analyzeFile(const QString &filePath, AudioFileModel &audioFile, int *const type)
224 *type = fileTypeNormal;
225 QFile readTest(filePath);
227 if (!readTest.open(QIODevice::ReadOnly))
229 *type = fileTypeDenied;
230 return audioFile;
233 if (checkFile_CDDA(readTest))
235 *type = fileTypeCDDA;
236 return audioFile;
239 readTest.close();
240 return analyzeMediaFile(filePath, audioFile);
243 const AudioFileModel& AnalyzeTask::analyzeMediaFile(const QString &filePath, AudioFileModel &audioFile)
245 //bool skipNext = false;
246 QPair<quint32, quint32> id_val(UINT_MAX, UINT_MAX);
247 quint32 coverType = UINT_MAX;
248 QByteArray coverData;
250 QStringList params;
251 params << QString("--Language=raw");
252 params << QString("-f");
253 params << QString("--Output=XML");
254 params << QDir::toNativeSeparators(filePath);
256 QProcess process;
257 MUtils::init_process(process, QFileInfo(m_mediaInfoBin).absolutePath());
258 process.start(m_mediaInfoBin, params);
260 QByteArray data;
261 data.reserve(16384);
263 if(!process.waitForStarted())
265 qWarning("MediaInfo process failed to create!");
266 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
267 process.kill();
268 process.waitForFinished(-1);
269 return audioFile;
272 while(process.state() != QProcess::NotRunning)
274 if(MUTILS_BOOLIFY(m_abortFlag))
276 process.kill();
277 qWarning("Process was aborted on user request!");
278 break;
281 if(!process.waitForReadyRead())
283 if(process.state() == QProcess::Running)
285 qWarning("MediaInfo time out. Killing the process now!");
286 process.kill();
287 process.waitForFinished(-1);
288 break;
292 forever
294 const QByteArray dataNext = process.readAll();
295 if (dataNext.isEmpty()) {
296 break; /*no more input data*/
298 data += dataNext;
302 process.waitForFinished();
303 if (process.state() != QProcess::NotRunning)
305 process.kill();
306 process.waitForFinished(-1);
309 while (!process.atEnd())
311 const QByteArray dataNext = process.readAll();
312 if (dataNext.isEmpty()) {
313 break; /*no more input data*/
315 data += dataNext;
318 //qDebug("!!!--START-->>>\n%s\n<<<--END--!!!", data.constData());
319 return parseMediaInfo(data, audioFile);
321 /* if(audioFile.metaInfo().title().isEmpty())
323 QString baseName = QFileInfo(filePath).fileName();
324 int index = baseName.lastIndexOf(".");
326 if(index >= 0)
328 baseName = baseName.left(index);
331 baseName = baseName.replace("_", " ").simplified();
332 index = baseName.lastIndexOf(" - ");
334 if(index >= 0)
336 baseName = baseName.mid(index + 3).trimmed();
339 audioFile.metaInfo().setTitle(baseName);
342 if((coverType != UINT_MAX) && (!coverData.isEmpty()))
344 retrieveCover(audioFile, coverType, coverData);
347 if((audioFile.techInfo().audioType().compare("PCM", Qt::CaseInsensitive) == 0) && (audioFile.techInfo().audioProfile().compare("Float", Qt::CaseInsensitive) == 0))
349 if(audioFile.techInfo().audioBitdepth() == 32) audioFile.techInfo().setAudioBitdepth(AudioFileModel::BITDEPTH_IEEE_FLOAT32);
352 return audioFile;*/
355 const AudioFileModel& AnalyzeTask::parseMediaInfo(const QByteArray &data, AudioFileModel &audioFile)
357 QXmlInputSource xmlSource;
358 xmlSource.setData(data);
360 QScopedPointer<QXmlDefaultHandler> xmlHandler(new AnalyzeTask_XmlHandler(audioFile));
362 QXmlSimpleReader xmlReader;
363 xmlReader.setContentHandler(xmlHandler.data());
364 xmlReader.setErrorHandler(xmlHandler.data());
366 if (xmlReader.parse(xmlSource))
368 while (xmlReader.parseContinue()) {/*continue*/}
371 if(audioFile.metaInfo().title().isEmpty())
373 QString baseName = QFileInfo(audioFile.filePath()).fileName();
374 int index;
375 if((index = baseName.lastIndexOf("."))>= 0)
377 baseName = baseName.left(index);
379 baseName = baseName.replace("_", " ").simplified();
380 if((index = baseName.lastIndexOf(" - ")) >= 0)
382 baseName = baseName.mid(index + 3).trimmed();
384 audioFile.metaInfo().setTitle(baseName);
387 if ((audioFile.techInfo().audioType().compare("PCM", Qt::CaseInsensitive) == 0) && (audioFile.techInfo().audioProfile().compare("Float", Qt::CaseInsensitive) == 0))
389 if (audioFile.techInfo().audioBitdepth() == 32) audioFile.techInfo().setAudioBitdepth(AudioFileModel::BITDEPTH_IEEE_FLOAT32);
392 return audioFile;
395 bool AnalyzeTask::checkFile_CDDA(QFile &file)
397 file.reset();
398 QByteArray data = file.read(128);
400 int i = data.indexOf("RIFF");
401 int j = data.indexOf("CDDA");
402 int k = data.indexOf("fmt ");
404 return ((i >= 0) && (j >= 0) && (k >= 0) && (k > j) && (j > i));
407 void AnalyzeTask::retrieveCover(AudioFileModel &audioFile, const quint32 coverType, const QByteArray &coverData)
409 qDebug("Retrieving cover! (MIME_TYPES_MAX=%u)", MIME_TYPES_MAX);
411 static const QString ext = QString::fromLatin1(MIME_TYPES[qBound(0U, coverType, MIME_TYPES_MAX)].ext[0]);
412 if(!(QImage::fromData(coverData, ext.toUpper().toLatin1().constData()).isNull()))
414 QFile coverFile(QString("%1/%2.%3").arg(MUtils::temp_folder(), MUtils::next_rand_str(), ext));
415 if(coverFile.open(QIODevice::WriteOnly))
417 coverFile.write(coverData);
418 coverFile.close();
419 audioFile.metaInfo().setCover(coverFile.fileName(), true);
422 else
424 qWarning("Image data seems to be invalid :-(");
428 bool AnalyzeTask::analyzeAvisynthFile(const QString &filePath, AudioFileModel &info)
430 QProcess process;
431 MUtils::init_process(process, QFileInfo(m_avs2wavBin).absolutePath());
433 process.start(m_avs2wavBin, QStringList() << QDir::toNativeSeparators(filePath) << "?");
435 if(!process.waitForStarted())
437 qWarning("AVS2WAV process failed to create!");
438 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
439 process.kill();
440 process.waitForFinished(-1);
441 return false;
444 bool bInfoHeaderFound = false;
446 while(process.state() != QProcess::NotRunning)
448 if(MUTILS_BOOLIFY(m_abortFlag))
450 process.kill();
451 qWarning("Process was aborted on user request!");
452 break;
455 if(!process.waitForReadyRead())
457 if(process.state() == QProcess::Running)
459 qWarning("AVS2WAV time out. Killing process and skipping file!");
460 process.kill();
461 process.waitForFinished(-1);
462 return false;
466 QByteArray data;
468 while(process.canReadLine())
470 QString line = QString::fromUtf8(process.readLine().constData()).simplified();
471 if(!line.isEmpty())
473 int index = line.indexOf(':');
474 if(index > 0)
476 QString key = line.left(index).trimmed();
477 QString val = line.mid(index+1).trimmed();
479 if(bInfoHeaderFound && !key.isEmpty() && !val.isEmpty())
481 if(key.compare("TotalSeconds", Qt::CaseInsensitive) == 0)
483 bool ok = false;
484 unsigned int duration = val.toUInt(&ok);
485 if(ok) info.techInfo().setDuration(duration);
487 if(key.compare("SamplesPerSec", Qt::CaseInsensitive) == 0)
489 bool ok = false;
490 unsigned int samplerate = val.toUInt(&ok);
491 if(ok) info.techInfo().setAudioSamplerate (samplerate);
493 if(key.compare("Channels", Qt::CaseInsensitive) == 0)
495 bool ok = false;
496 unsigned int channels = val.toUInt(&ok);
497 if(ok) info.techInfo().setAudioChannels(channels);
499 if(key.compare("BitsPerSample", Qt::CaseInsensitive) == 0)
501 bool ok = false;
502 unsigned int bitdepth = val.toUInt(&ok);
503 if(ok) info.techInfo().setAudioBitdepth(bitdepth);
507 else
509 if(line.contains("[Audio Info]", Qt::CaseInsensitive))
511 info.techInfo().setAudioType("Avisynth");
512 info.techInfo().setContainerType("Avisynth");
513 bInfoHeaderFound = true;
520 process.waitForFinished();
521 if(process.state() != QProcess::NotRunning)
523 process.kill();
524 process.waitForFinished(-1);
527 //Check exit code
528 switch(process.exitCode())
530 case 0:
531 qDebug("Avisynth script was analyzed successfully.");
532 return true;
533 break;
534 case -5:
535 qWarning("It appears that Avisynth is not installed on the system!");
536 return false;
537 break;
538 default:
539 qWarning("Failed to open the Avisynth script, bad AVS file?");
540 return false;
541 break;
545 unsigned int AnalyzeTask::parseYear(const QString &str)
547 if(str.startsWith("UTC", Qt::CaseInsensitive))
549 QDate date = QDate::fromString(str.mid(3).trimmed().left(10), "yyyy-MM-dd");
550 if(date.isValid())
552 return date.year();
554 else
556 return 0;
559 else
561 bool ok = false;
562 int year = str.toInt(&ok);
563 if(ok && year > 0)
565 return year;
567 else
569 return 0;
574 // ---------------------------------------------------------
575 // XML Content Handler Implementation
576 // ---------------------------------------------------------
578 #define DEFINE_PROPTERY_MAPPING(TYPE, NAME) do \
580 builder->insert(qMakePair(trackType_##TYPE, QString::fromLatin1(#NAME)), propertyId_##TYPE##_##NAME); \
582 while(0)
584 #define SET_OPTIONAL(TYPE, IF_CMD, THEN_CMD) do \
586 TYPE _tmp;\
587 if((IF_CMD)) { THEN_CMD; } \
589 while(0)
591 QReadWriteLock AnalyzeTask_XmlHandler::s_propertiesMutex;
592 QScopedPointer<const QMap<QPair<AnalyzeTask_XmlHandler::trackType_t, QString>, AnalyzeTask_XmlHandler::propertyId_t>> AnalyzeTask_XmlHandler::s_propertiesMap;
594 const QMap<QPair<AnalyzeTask_XmlHandler::trackType_t, QString>, AnalyzeTask_XmlHandler::propertyId_t> &AnalyzeTask_XmlHandler::initializeProperties(void)
596 QReadLocker rdLocker(&s_propertiesMutex);
597 if (!s_propertiesMap.isNull())
599 return *s_propertiesMap.data();
602 rdLocker.unlock();
603 QWriteLocker wrLocker(&s_propertiesMutex);
605 if (s_propertiesMap.isNull())
607 qWarning("!!! --- SETTING UP MAP --- !!!");
608 QMap<QPair<trackType_t, QString>, propertyId_t> *const builder = new QMap<QPair<trackType_t, QString>, propertyId_t>();
609 DEFINE_PROPTERY_MAPPING(gen, format);
610 DEFINE_PROPTERY_MAPPING(gen, format_profile);
611 DEFINE_PROPTERY_MAPPING(gen, duration);
612 DEFINE_PROPTERY_MAPPING(aud, format);
613 DEFINE_PROPTERY_MAPPING(aud, format_version);
614 DEFINE_PROPTERY_MAPPING(aud, format_profile);
615 DEFINE_PROPTERY_MAPPING(aud, channel_s_);
616 DEFINE_PROPTERY_MAPPING(aud, samplingrate);
617 s_propertiesMap.reset(builder);
620 return *s_propertiesMap.data();
623 bool AnalyzeTask_XmlHandler::startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &atts)
625 m_stack.push(qName);
626 switch (m_stack.size())
628 case 1:
629 if (!STR_EQ(qName, "mediaInfo"))
631 qWarning("Invalid XML structure was detected! (1)");
632 return false;
634 return true;
635 case 2:
636 if (!STR_EQ(qName, "file"))
638 qWarning("Invalid XML structure was detected! (2)");
639 return false;
641 return true;
642 case 3:
643 if (!STR_EQ(qName, "track"))
645 qWarning("Invalid XML structure was detected! (3)");
646 return false;
648 else
650 const QString value = atts.value("type").trimmed();
651 if (STR_EQ(value, "general"))
653 m_trackType = trackType_gen;
655 else if (STR_EQ(value, "audio"))
657 if (m_trackIdx++)
659 qWarning("Skipping non-primary audio track!");
660 m_trackType = trackType_non;
662 else
664 m_trackType = trackType_aud;
667 else /*e.g. video*/
669 qWarning("Skipping a non-audio track!");
670 m_trackType = trackType_non;
672 return true;
674 case 4:
675 switch (m_trackType)
677 case trackType_gen:
678 m_currentProperty = qMakePair(trackType_gen, qName.simplified().toLower());
679 return true;
680 case trackType_aud:
681 m_currentProperty = qMakePair(trackType_aud, qName.simplified().toLower());
682 return true;
683 default:
684 m_currentProperty = qMakePair(trackType_non, qName.simplified().toLower());
685 return true;
687 default:
688 return false;
692 bool AnalyzeTask_XmlHandler::endElement(const QString &namespaceURI, const QString &localName, const QString &qName)
694 m_stack.pop();
695 return true;
698 bool AnalyzeTask_XmlHandler::characters(const QString& ch)
700 if ((m_stack.size() == 4) && m_currentProperty.first)
702 const QString value = ch.simplified();
703 if (!value.isEmpty())
705 qDebug("Property: %u::%s --> \"%s\"", m_currentProperty.first, MUTILS_UTF8(m_currentProperty.second), MUTILS_UTF8(value));
706 if (!updatePropertry(m_currentProperty, value))
708 qWarning("Ignored property: %u::%s!", m_currentProperty.first, MUTILS_UTF8(m_currentProperty.second));
712 return true;
715 bool AnalyzeTask_XmlHandler::updatePropertry(const QPair<trackType_t, QString> &id, const QString &value)
717 switch (m_properties.value(id, propertyId_t(-1)))
719 case propertyId_gen_format: m_audioFile.techInfo().setContainerType(value); return true;
720 case propertyId_gen_format_profile: m_audioFile.techInfo().setContainerProfile(value); return true;
721 case propertyId_gen_duration: SET_OPTIONAL(quint32, parseUnsigned(value, _tmp), m_audioFile.techInfo().setDuration(decodeTime(_tmp))); return true;
722 case propertyId_aud_format: m_audioFile.techInfo().setAudioType(value); return true;
723 case propertyId_aud_format_version: m_audioFile.techInfo().setAudioVersion(value); return true;
724 case propertyId_aud_format_profile: m_audioFile.techInfo().setAudioProfile(value); return true;
725 case propertyId_aud_channel_s_: SET_OPTIONAL(quint32, parseUnsigned(value, _tmp), m_audioFile.techInfo().setAudioChannels(_tmp)); return true;
726 case propertyId_aud_samplingrate: SET_OPTIONAL(quint32, parseUnsigned(value, _tmp), m_audioFile.techInfo().setAudioSamplerate(_tmp)); return true;
727 default: return false;
731 bool AnalyzeTask_XmlHandler::parseUnsigned(const QString &str, quint32 &value)
733 bool okay = false;
734 value = str.toUInt(&okay);
735 return okay;
738 quint32 AnalyzeTask_XmlHandler::decodeTime(quint32 &value)
740 return (value + 500U) / 1000U;
743 ////////////////////////////////////////////////////////////
744 // Public Functions
745 ////////////////////////////////////////////////////////////
747 /*NONE*/
749 ////////////////////////////////////////////////////////////
750 // EVENTS
751 ////////////////////////////////////////////////////////////
753 /*NONE*/