Adapt for latest MUtils changes.
[LameXP.git] / src / Thread_FileAnalyzer_Task.cpp
bloba49c8d3694413fed79049cf24757bdad06ebcbf4
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2016 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>
49 //CRT
50 #include <math.h>
51 #include <time.h>
52 #include <assert.h>
54 #define IS_KEY(KEY) (key.compare(KEY, Qt::CaseInsensitive) == 0)
55 #define IS_SEC(SEC) (key.startsWith((SEC "_"), Qt::CaseInsensitive))
56 #define FIRST_TOK(STR) (STR.split(" ", QString::SkipEmptyParts).first())
58 ////////////////////////////////////////////////////////////
59 // Constructor
60 ////////////////////////////////////////////////////////////
62 AnalyzeTask::AnalyzeTask(const int taskId, const QString &inputFile, const QString &templateFile, volatile bool *abortFlag)
64 m_taskId(taskId),
65 m_inputFile(inputFile),
66 m_templateFile(templateFile),
67 m_mediaInfoBin(lamexp_tools_lookup("mediainfo.exe")),
68 m_avs2wavBin(lamexp_tools_lookup("avs2wav.exe")),
69 m_abortFlag(abortFlag)
71 if(m_mediaInfoBin.isEmpty() || m_avs2wavBin.isEmpty())
73 qFatal("Invalid path to MediaInfo binary. Tool not initialized properly.");
77 AnalyzeTask::~AnalyzeTask(void)
79 emit taskCompleted(m_taskId);
82 ////////////////////////////////////////////////////////////
83 // Thread Main
84 ////////////////////////////////////////////////////////////
86 void AnalyzeTask::run()
88 try
90 run_ex();
92 catch(const std::exception &error)
94 MUTILS_PRINT_ERROR("\nGURU MEDITATION !!!\n\nException error:\n%s\n", error.what());
95 MUtils::OS::fatal_exit(L"Unhandeled C++ exception error, application will exit!");
97 catch(...)
99 MUTILS_PRINT_ERROR("\nGURU MEDITATION !!!\n\nUnknown exception error!\n");
100 MUtils::OS::fatal_exit(L"Unhandeled C++ exception error, application will exit!");
104 void AnalyzeTask::run_ex(void)
106 int fileType = fileTypeNormal;
107 QString currentFile = QDir::fromNativeSeparators(m_inputFile);
108 qDebug("Analyzing: %s", MUTILS_UTF8(currentFile));
110 AudioFileModel file = analyzeFile(currentFile, &fileType);
112 if(*m_abortFlag)
114 qWarning("Operation cancelled by user!");
115 return;
118 switch(fileType)
120 case fileTypeDenied:
121 qWarning("Cannot access file for reading, skipping!");
122 break;
123 case fileTypeCDDA:
124 qWarning("Dummy CDDA file detected, skipping!");
125 break;
126 default:
127 if(file.metaInfo().title().isEmpty() || file.techInfo().containerType().isEmpty() || file.techInfo().audioType().isEmpty())
129 fileType = fileTypeUnknown;
130 if(!QFileInfo(currentFile).suffix().compare("cue", Qt::CaseInsensitive))
132 qWarning("Cue Sheet file detected, skipping!");
133 fileType = fileTypeCueSheet;
135 else if(!QFileInfo(currentFile).suffix().compare("avs", Qt::CaseInsensitive))
137 qDebug("Found a potential Avisynth script, investigating...");
138 if(analyzeAvisynthFile(currentFile, file))
140 fileType = fileTypeNormal;
142 else
144 qDebug("Rejected Avisynth file: %s", MUTILS_UTF8(file.filePath()));
147 else
149 qDebug("Rejected file of unknown type: %s", MUTILS_UTF8(file.filePath()));
152 break;
155 //Emit the file now!
156 emit fileAnalyzed(m_taskId, fileType, file);
159 ////////////////////////////////////////////////////////////
160 // Privtae Functions
161 ////////////////////////////////////////////////////////////
163 const AudioFileModel AnalyzeTask::analyzeFile(const QString &filePath, int *type)
165 *type = fileTypeNormal;
166 AudioFileModel audioFile(filePath);
168 QFile readTest(filePath);
169 if(!readTest.open(QIODevice::ReadOnly))
171 *type = fileTypeDenied;
172 return audioFile;
174 if(checkFile_CDDA(readTest))
176 *type = fileTypeCDDA;
177 return audioFile;
179 readTest.close();
181 bool skipNext = false;
182 QPair<quint32, quint32> id_val(UINT_MAX, UINT_MAX);
183 quint32 coverType = UINT_MAX;
184 QByteArray coverData;
186 QStringList params;
187 params << QString("--Inform=file://%1").arg(QDir::toNativeSeparators(m_templateFile));
188 params << QDir::toNativeSeparators(filePath);
190 QProcess process;
191 MUtils::init_process(process, QFileInfo(m_mediaInfoBin).absolutePath());
193 process.start(m_mediaInfoBin, params);
195 if(!process.waitForStarted())
197 qWarning("MediaInfo process failed to create!");
198 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
199 process.kill();
200 process.waitForFinished(-1);
201 return audioFile;
204 while(process.state() != QProcess::NotRunning)
206 if(*m_abortFlag)
208 process.kill();
209 qWarning("Process was aborted on user request!");
210 break;
213 if(!process.waitForReadyRead())
215 if(process.state() == QProcess::Running)
217 qWarning("MediaInfo time out. Killing process and skipping file!");
218 process.kill();
219 process.waitForFinished(-1);
220 return audioFile;
224 QByteArray data;
226 while(process.canReadLine())
228 QString line = QString::fromUtf8(process.readLine().constData()).simplified();
229 if(!line.isEmpty())
231 //qDebug("Line:%s", MUTILS_UTF8(line));
233 int index = line.indexOf('=');
234 if(index > 0)
236 QString key = line.left(index).trimmed();
237 QString val = line.mid(index+1).trimmed();
238 if(!key.isEmpty())
240 updateInfo(audioFile, skipNext, id_val, coverType, coverData, key, val);
247 if(audioFile.metaInfo().title().isEmpty())
249 QString baseName = QFileInfo(filePath).fileName();
250 int index = baseName.lastIndexOf(".");
252 if(index >= 0)
254 baseName = baseName.left(index);
257 baseName = baseName.replace("_", " ").simplified();
258 index = baseName.lastIndexOf(" - ");
260 if(index >= 0)
262 baseName = baseName.mid(index + 3).trimmed();
265 audioFile.metaInfo().setTitle(baseName);
268 process.waitForFinished();
269 if(process.state() != QProcess::NotRunning)
271 process.kill();
272 process.waitForFinished(-1);
275 if((coverType != UINT_MAX) && (!coverData.isEmpty()))
277 retrieveCover(audioFile, coverType, coverData);
280 if((audioFile.techInfo().audioType().compare("PCM", Qt::CaseInsensitive) == 0) && (audioFile.techInfo().audioProfile().compare("Float", Qt::CaseInsensitive) == 0))
282 if(audioFile.techInfo().audioBitdepth() == 32) audioFile.techInfo().setAudioBitdepth(AudioFileModel::BITDEPTH_IEEE_FLOAT32);
285 return audioFile;
288 void AnalyzeTask::updateInfo(AudioFileModel &audioFile, bool &skipNext, QPair<quint32, quint32> &id_val, quint32 &coverType, QByteArray &coverData, const QString &key, const QString &value)
290 //qWarning("'%s' -> '%s'", MUTILS_UTF8(key), MUTILS_UTF8(value));
292 /*New Stream*/
293 if(IS_KEY("Gen_ID") || IS_KEY("Aud_ID"))
295 if(value.isEmpty())
297 skipNext = false;
299 else
301 //We ignore all ID's, except for the lowest one!
302 bool ok = false;
303 unsigned int id = value.toUInt(&ok);
304 if(ok)
306 if(IS_KEY("Gen_ID")) { id_val.first = qMin(id_val.first, id); skipNext = (id > id_val.first); }
307 if(IS_KEY("Aud_ID")) { id_val.second = qMin(id_val.second, id); skipNext = (id > id_val.second); }
309 else
311 skipNext = true;
314 if(skipNext)
316 qWarning("Skipping info for non-primary stream!");
318 return;
321 /*Skip or empty?*/
322 if((skipNext) || value.isEmpty())
324 return;
327 /*Playlist file?*/
328 if(IS_KEY("Aud_Source"))
330 skipNext = true;
331 audioFile.techInfo().setContainerType(QString());
332 audioFile.techInfo().setAudioType(QString());
333 qWarning("Skipping info for playlist file!");
334 return;
337 /*General Section*/
338 if(IS_SEC("Gen"))
340 if(IS_KEY("Gen_Format"))
342 audioFile.techInfo().setContainerType(value);
344 else if(IS_KEY("Gen_Format_Profile"))
346 audioFile.techInfo().setContainerProfile(value);
348 else if(IS_KEY("Gen_Title") || IS_KEY("Gen_Track"))
350 audioFile.metaInfo().setTitle(value);
352 else if(IS_KEY("Gen_Duration"))
354 unsigned int tmp = parseDuration(value);
355 if(tmp > 0) audioFile.techInfo().setDuration(tmp);
357 else if(IS_KEY("Gen_Artist") || IS_KEY("Gen_Performer"))
359 audioFile.metaInfo().setArtist(value);
361 else if(IS_KEY("Gen_Album"))
363 audioFile.metaInfo().setAlbum(value);
365 else if(IS_KEY("Gen_Genre"))
367 audioFile.metaInfo().setGenre(value);
369 else if(IS_KEY("Gen_Released_Date") || IS_KEY("Gen_Recorded_Date"))
371 unsigned int tmp = parseYear(value);
372 if(tmp > 0) audioFile.metaInfo().setYear(tmp);
374 else if(IS_KEY("Gen_Comment"))
376 audioFile.metaInfo().setComment(value);
378 else if(IS_KEY("Gen_Track/Position"))
380 bool ok = false;
381 unsigned int tmp = value.toUInt(&ok);
382 if(ok) audioFile.metaInfo().setPosition(tmp);
384 else if(IS_KEY("Gen_Cover") || IS_KEY("Gen_Cover_Type"))
386 if(coverType == UINT_MAX)
388 coverType = 0;
391 else if(IS_KEY("Gen_Cover_Mime"))
393 QString temp = FIRST_TOK(value);
394 for (quint32 i = 0; MIME_TYPES[i].type; i++)
396 if (temp.compare(QString::fromLatin1(MIME_TYPES[i].type), Qt::CaseInsensitive) == 0)
398 coverType = i;
399 break;
403 else if(IS_KEY("Gen_Cover_Data"))
405 if(!coverData.isEmpty()) coverData.clear();
406 coverData.append(QByteArray::fromBase64(FIRST_TOK(value).toLatin1()));
408 else
410 qWarning("Unknown key '%s' with value '%s' found!", MUTILS_UTF8(key), MUTILS_UTF8(value));
412 return;
415 /*Audio Section*/
416 if(IS_SEC("Aud"))
419 if(IS_KEY("Aud_Format"))
421 audioFile.techInfo().setAudioType(value);
423 else if(IS_KEY("Aud_Format_Profile"))
425 audioFile.techInfo().setAudioProfile(value);
427 else if(IS_KEY("Aud_Format_Version"))
429 audioFile.techInfo().setAudioVersion(value);
431 else if(IS_KEY("Aud_Channel(s)"))
433 bool ok = false;
434 unsigned int tmp = value.toUInt(&ok);
435 if(ok) audioFile.techInfo().setAudioChannels(tmp);
437 else if(IS_KEY("Aud_SamplingRate"))
439 bool ok = false;
440 unsigned int tmp = value.toUInt(&ok);
441 if(ok) audioFile.techInfo().setAudioSamplerate(tmp);
443 else if(IS_KEY("Aud_BitDepth"))
445 bool ok = false;
446 unsigned int tmp = value.toUInt(&ok);
447 if(ok) audioFile.techInfo().setAudioBitdepth(tmp);
449 else if(IS_KEY("Aud_Duration"))
451 unsigned int tmp = parseDuration(value);
452 if(tmp > 0) audioFile.techInfo().setDuration(tmp);
454 else if(IS_KEY("Aud_BitRate"))
456 bool ok = false;
457 unsigned int tmp = value.toUInt(&ok);
458 if(ok) audioFile.techInfo().setAudioBitrate(tmp/1000);
460 else if(IS_KEY("Aud_BitRate_Mode"))
462 if(!value.compare("CBR", Qt::CaseInsensitive)) audioFile.techInfo().setAudioBitrateMode(AudioFileModel::BitrateModeConstant);
463 if(!value.compare("VBR", Qt::CaseInsensitive)) audioFile.techInfo().setAudioBitrateMode(AudioFileModel::BitrateModeVariable);
465 else if(IS_KEY("Aud_Encoded_Library"))
467 audioFile.techInfo().setAudioEncodeLib(value);
469 else
471 qWarning("Unknown key '%s' with value '%s' found!", MUTILS_UTF8(key), MUTILS_UTF8(value));
473 return;
476 /*Section not recognized*/
477 qWarning("Unknown section: %s", MUTILS_UTF8(key));
480 bool AnalyzeTask::checkFile_CDDA(QFile &file)
482 file.reset();
483 QByteArray data = file.read(128);
485 int i = data.indexOf("RIFF");
486 int j = data.indexOf("CDDA");
487 int k = data.indexOf("fmt ");
489 return ((i >= 0) && (j >= 0) && (k >= 0) && (k > j) && (j > i));
492 void AnalyzeTask::retrieveCover(AudioFileModel &audioFile, const quint32 coverType, const QByteArray &coverData)
494 qDebug("Retrieving cover! (MIME_TYPES_MAX=%u)", MIME_TYPES_MAX);
496 static const QString ext = QString::fromLatin1(MIME_TYPES[qBound(0U, coverType, MIME_TYPES_MAX)].ext[0]);
497 if(!(QImage::fromData(coverData, ext.toUpper().toLatin1().constData()).isNull()))
499 QFile coverFile(QString("%1/%2.%3").arg(MUtils::temp_folder(), MUtils::next_rand_str(), ext));
500 if(coverFile.open(QIODevice::WriteOnly))
502 coverFile.write(coverData);
503 coverFile.close();
504 audioFile.metaInfo().setCover(coverFile.fileName(), true);
507 else
509 qWarning("Image data seems to be invalid :-(");
513 bool AnalyzeTask::analyzeAvisynthFile(const QString &filePath, AudioFileModel &info)
515 QProcess process;
516 MUtils::init_process(process, QFileInfo(m_avs2wavBin).absolutePath());
518 process.start(m_avs2wavBin, QStringList() << QDir::toNativeSeparators(filePath) << "?");
520 if(!process.waitForStarted())
522 qWarning("AVS2WAV process failed to create!");
523 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
524 process.kill();
525 process.waitForFinished(-1);
526 return false;
529 bool bInfoHeaderFound = false;
531 while(process.state() != QProcess::NotRunning)
533 if(*m_abortFlag)
535 process.kill();
536 qWarning("Process was aborted on user request!");
537 break;
540 if(!process.waitForReadyRead())
542 if(process.state() == QProcess::Running)
544 qWarning("AVS2WAV time out. Killing process and skipping file!");
545 process.kill();
546 process.waitForFinished(-1);
547 return false;
551 QByteArray data;
553 while(process.canReadLine())
555 QString line = QString::fromUtf8(process.readLine().constData()).simplified();
556 if(!line.isEmpty())
558 int index = line.indexOf(':');
559 if(index > 0)
561 QString key = line.left(index).trimmed();
562 QString val = line.mid(index+1).trimmed();
564 if(bInfoHeaderFound && !key.isEmpty() && !val.isEmpty())
566 if(key.compare("TotalSeconds", Qt::CaseInsensitive) == 0)
568 bool ok = false;
569 unsigned int duration = val.toUInt(&ok);
570 if(ok) info.techInfo().setDuration(duration);
572 if(key.compare("SamplesPerSec", Qt::CaseInsensitive) == 0)
574 bool ok = false;
575 unsigned int samplerate = val.toUInt(&ok);
576 if(ok) info.techInfo().setAudioSamplerate (samplerate);
578 if(key.compare("Channels", Qt::CaseInsensitive) == 0)
580 bool ok = false;
581 unsigned int channels = val.toUInt(&ok);
582 if(ok) info.techInfo().setAudioChannels(channels);
584 if(key.compare("BitsPerSample", Qt::CaseInsensitive) == 0)
586 bool ok = false;
587 unsigned int bitdepth = val.toUInt(&ok);
588 if(ok) info.techInfo().setAudioBitdepth(bitdepth);
592 else
594 if(line.contains("[Audio Info]", Qt::CaseInsensitive))
596 info.techInfo().setAudioType("Avisynth");
597 info.techInfo().setContainerType("Avisynth");
598 bInfoHeaderFound = true;
605 process.waitForFinished();
606 if(process.state() != QProcess::NotRunning)
608 process.kill();
609 process.waitForFinished(-1);
612 //Check exit code
613 switch(process.exitCode())
615 case 0:
616 qDebug("Avisynth script was analyzed successfully.");
617 return true;
618 break;
619 case -5:
620 qWarning("It appears that Avisynth is not installed on the system!");
621 return false;
622 break;
623 default:
624 qWarning("Failed to open the Avisynth script, bad AVS file?");
625 return false;
626 break;
630 unsigned int AnalyzeTask::parseYear(const QString &str)
632 if(str.startsWith("UTC", Qt::CaseInsensitive))
634 QDate date = QDate::fromString(str.mid(3).trimmed().left(10), "yyyy-MM-dd");
635 if(date.isValid())
637 return date.year();
639 else
641 return 0;
644 else
646 bool ok = false;
647 int year = str.toInt(&ok);
648 if(ok && year > 0)
650 return year;
652 else
654 return 0;
659 unsigned int AnalyzeTask::parseDuration(const QString &str)
661 bool ok = false;
662 unsigned int value = str.toUInt(&ok);
663 return ok ? (value/1000) : 0;
667 ////////////////////////////////////////////////////////////
668 // Public Functions
669 ////////////////////////////////////////////////////////////
671 /*NONE*/
673 ////////////////////////////////////////////////////////////
674 // EVENTS
675 ////////////////////////////////////////////////////////////
677 /*NONE*/