Updated MediaInfo binary to v0.7.57 (2012-05-02), compiled with ICL 12.1.7 and MSVC...
[LameXP.git] / src / Thread_FileAnalyzer_Task.cpp
blob4ba84e88bfef043acd25eef5e2e7fe4e3a57ee3f
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2012 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.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License along
16 // with this program; if not, write to the Free Software Foundation, Inc.,
17 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 // http://www.gnu.org/licenses/gpl-2.0.txt
20 ///////////////////////////////////////////////////////////////////////////////
22 #include "Thread_FileAnalyzer_Task.h"
24 #include "Global.h"
25 #include "LockedFile.h"
26 #include "Model_AudioFile.h"
27 #include "PlaylistImporter.h"
29 #include <QDir>
30 #include <QFileInfo>
31 #include <QProcess>
32 #include <QDate>
33 #include <QTime>
34 #include <QDebug>
35 #include <QImage>
36 #include <QReadLocker>
37 #include <QWriteLocker>
38 #include <QThread>
40 #include <math.h>
41 #include <time.h>
43 #define IS_KEY(KEY) (key.compare(KEY, Qt::CaseInsensitive) == 0)
44 #define IS_SEC(SEC) (key.startsWith((SEC "_"), Qt::CaseInsensitive))
45 #define FIRST_TOK(STR) (STR.split(" ", QString::SkipEmptyParts).first())
47 /* static vars */
48 QReadWriteLock AnalyzeTask::s_lock;
49 QMutex AnalyzeTask::s_waitMutex;
50 QWaitCondition AnalyzeTask::s_waitCond;
51 unsigned __int64 AnalyzeTask::s_threadIdx_created;
52 unsigned __int64 AnalyzeTask::s_threadIdx_finished;
53 unsigned int AnalyzeTask::s_filesAccepted;
54 unsigned int AnalyzeTask::s_filesRejected;
55 unsigned int AnalyzeTask::s_filesDenied;
56 unsigned int AnalyzeTask::s_filesDummyCDDA;
57 unsigned int AnalyzeTask::s_filesCueSheet;
58 QStringList AnalyzeTask::s_additionalFiles;
59 QStringList AnalyzeTask::s_recentlyAdded;
61 ////////////////////////////////////////////////////////////
62 // Constructor
63 ////////////////////////////////////////////////////////////
65 AnalyzeTask::AnalyzeTask(const QString &inputFile, const QString &templateFile, volatile bool *abortFlag)
67 m_threadIdx(makeThreadIdx()),
68 m_inputFile(inputFile),
69 m_templateFile(templateFile),
70 m_mediaInfoBin(lamexp_lookup_tool("mediainfo.exe")),
71 m_avs2wavBin(lamexp_lookup_tool("avs2wav.exe")),
72 m_abortFlag(abortFlag)
74 if(m_mediaInfoBin.isEmpty() || m_avs2wavBin.isEmpty())
76 qFatal("Invalid path to MediaInfo binary. Tool not initialized properly.");
80 AnalyzeTask::~AnalyzeTask(void)
82 s_waitMutex.lock();
83 s_threadIdx_finished = qMax(s_threadIdx_finished, m_threadIdx + 1ui64);
84 s_waitMutex.unlock();
86 s_waitCond.wakeAll();
89 ////////////////////////////////////////////////////////////
90 // Thread Main
91 ////////////////////////////////////////////////////////////
93 void AnalyzeTask::run()
95 try
97 run_ex();
99 catch(...)
101 qWarning("WARNING: Caught an in exception AnalyzeTask thread!");
104 s_waitMutex.lock();
105 s_threadIdx_finished = qMax(s_threadIdx_finished, m_threadIdx + 1ui64);
106 s_waitMutex.unlock();
108 s_waitCond.wakeAll();
111 void AnalyzeTask::run_ex(void)
113 int fileType = fileTypeNormal;
114 QString currentFile = QDir::fromNativeSeparators(m_inputFile);
115 qDebug("Analyzing: %s", currentFile.toUtf8().constData());
117 emit fileSelected(QFileInfo(currentFile).fileName());
118 emit progressValChanged(m_threadIdx + 1);
120 AudioFileModel file = analyzeFile(currentFile, &fileType);
122 if(*m_abortFlag)
124 qWarning("Operation cancelled by user!");
125 return;
127 if(fileType == fileTypeSkip)
129 qWarning("File was recently added, skipping!");
130 return;
132 if(fileType == fileTypeDenied)
134 QWriteLocker lock(&s_lock);
135 s_filesDenied++;
136 qWarning("Cannot access file for reading, skipping!");
137 return;
139 if(fileType == fileTypeCDDA)
141 QWriteLocker lock(&s_lock);
142 s_filesDummyCDDA++;
143 qWarning("Dummy CDDA file detected, skipping!");
144 return;
147 if(file.fileName().isEmpty() || file.formatContainerType().isEmpty() || file.formatAudioType().isEmpty())
149 QStringList fileList;
150 if(PlaylistImporter::importPlaylist(fileList, currentFile))
152 waitForPreviousThreads();
153 qDebug("Imported playlist file.");
154 QWriteLocker lock(&s_lock);
155 s_additionalFiles << fileList;
157 else if(!QFileInfo(currentFile).suffix().compare("cue", Qt::CaseInsensitive))
159 QWriteLocker lock(&s_lock);
160 qWarning("Cue Sheet file detected, skipping!");
161 s_filesCueSheet++;
163 else if(!QFileInfo(currentFile).suffix().compare("avs", Qt::CaseInsensitive))
165 qDebug("Found a potential Avisynth script, investigating...");
166 if(analyzeAvisynthFile(currentFile, file))
168 QWriteLocker lock(&s_lock);
169 s_filesAccepted++;
170 s_recentlyAdded.append(file.filePath());
171 lock.unlock();
173 waitForPreviousThreads();
174 emit fileAnalyzed(file);
176 else
178 QWriteLocker lock(&s_lock);
179 qDebug("Rejected Avisynth file: %s", file.filePath().toUtf8().constData());
180 s_filesRejected++;
183 else
185 QWriteLocker lock(&s_lock);
186 qDebug("Rejected file of unknown type: %s", file.filePath().toUtf8().constData());
187 s_filesRejected++;
189 return;
192 QWriteLocker lock(&s_lock);
193 s_filesAccepted++;
194 s_recentlyAdded.append(file.filePath());
195 lock.unlock();
197 waitForPreviousThreads();
198 emit fileAnalyzed(file);
201 ////////////////////////////////////////////////////////////
202 // Privtae Functions
203 ////////////////////////////////////////////////////////////
205 const AudioFileModel AnalyzeTask::analyzeFile(const QString &filePath, int *type)
207 *type = fileTypeNormal;
208 AudioFileModel audioFile(filePath);
210 QReadLocker readLock(&s_lock);
211 if(s_recentlyAdded.contains(filePath, Qt::CaseInsensitive))
213 *type = fileTypeSkip;
214 return audioFile;
216 readLock.unlock();
218 QFile readTest(filePath);
219 if(!readTest.open(QIODevice::ReadOnly))
221 *type = fileTypeDenied;
222 return audioFile;
224 if(checkFile_CDDA(readTest))
226 *type = fileTypeCDDA;
227 return audioFile;
229 readTest.close();
231 bool skipNext = false;
232 unsigned int id_val[2] = {UINT_MAX, UINT_MAX};
233 cover_t coverType = coverNone;
234 QByteArray coverData;
236 QStringList params;
237 params << QString("--Inform=file://%1").arg(QDir::toNativeSeparators(m_templateFile));
238 params << QDir::toNativeSeparators(filePath);
240 QProcess process;
241 process.setProcessChannelMode(QProcess::MergedChannels);
242 process.setReadChannel(QProcess::StandardOutput);
243 process.start(m_mediaInfoBin, params);
245 if(!process.waitForStarted())
247 qWarning("MediaInfo process failed to create!");
248 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
249 process.kill();
250 process.waitForFinished(-1);
251 return audioFile;
254 while(process.state() != QProcess::NotRunning)
256 if(*m_abortFlag)
258 process.kill();
259 qWarning("Process was aborted on user request!");
260 break;
263 if(!process.waitForReadyRead())
265 if(process.state() == QProcess::Running)
267 qWarning("MediaInfo time out. Killing process and skipping file!");
268 process.kill();
269 process.waitForFinished(-1);
270 return audioFile;
274 QByteArray data;
276 while(process.canReadLine())
278 QString line = QString::fromUtf8(process.readLine().constData()).simplified();
279 if(!line.isEmpty())
281 //qDebug("Line:%s", line.toUtf8().constData());
283 int index = line.indexOf('=');
284 if(index > 0)
286 QString key = line.left(index).trimmed();
287 QString val = line.mid(index+1).trimmed();
288 if(!key.isEmpty())
290 updateInfo(audioFile, &skipNext, id_val, &coverType, &coverData, key, val);
297 if(audioFile.fileName().isEmpty())
299 QString baseName = QFileInfo(filePath).fileName();
300 int index = baseName.lastIndexOf(".");
302 if(index >= 0)
304 baseName = baseName.left(index);
307 baseName = baseName.replace("_", " ").simplified();
308 index = baseName.lastIndexOf(" - ");
310 if(index >= 0)
312 baseName = baseName.mid(index + 3).trimmed();
315 audioFile.setFileName(baseName);
318 process.waitForFinished();
319 if(process.state() != QProcess::NotRunning)
321 process.kill();
322 process.waitForFinished(-1);
325 if((coverType != coverNone) && (!coverData.isEmpty()))
327 retrieveCover(audioFile, coverType, coverData);
330 return audioFile;
333 void AnalyzeTask::updateInfo(AudioFileModel &audioFile, bool *skipNext, unsigned int *id_val, cover_t *coverType, QByteArray *coverData, const QString &key, const QString &value)
335 //qWarning("'%s' -> '%s'", key.toUtf8().constData(), value.toUtf8().constData());
337 /*New Stream*/
338 if(IS_KEY("Gen_ID") || IS_KEY("Aud_ID"))
340 if(value.isEmpty())
342 *skipNext = false;
344 else
346 //We ignore all ID's, except for the lowest one!
347 bool ok = false;
348 unsigned int id = value.toUInt(&ok);
349 if(ok)
351 if(IS_KEY("Gen_ID")) { id_val[0] = qMin(id_val[0], id); *skipNext = (id > id_val[0]); }
352 if(IS_KEY("Aud_ID")) { id_val[1] = qMin(id_val[1], id); *skipNext = (id > id_val[1]); }
354 else
356 *skipNext = true;
359 if(*skipNext)
361 qWarning("Skipping info for non-primary stream!");
363 return;
366 /*Skip or empty?*/
367 if((*skipNext) || value.isEmpty())
369 return;
372 /*Playlist file?*/
373 if(IS_KEY("Aud_Source"))
375 *skipNext = true;
376 audioFile.setFormatContainerType(QString());
377 audioFile.setFormatAudioType(QString());
378 qWarning("Skipping info for playlist file!");
379 return;
382 /*General Section*/
383 if(IS_SEC("Gen"))
385 if(IS_KEY("Gen_Format"))
387 audioFile.setFormatContainerType(value);
389 else if(IS_KEY("Gen_Format_Profile"))
391 audioFile.setFormatContainerProfile(value);
393 else if(IS_KEY("Gen_Title") || IS_KEY("Gen_Track"))
395 audioFile.setFileName(value);
397 else if(IS_KEY("Gen_Duration"))
399 unsigned int tmp = parseDuration(value);
400 if(tmp > 0) audioFile.setFileDuration(tmp);
402 else if(IS_KEY("Gen_Artist") || IS_KEY("Gen_Performer"))
404 audioFile.setFileArtist(value);
406 else if(IS_KEY("Gen_Album"))
408 audioFile.setFileAlbum(value);
410 else if(IS_KEY("Gen_Genre"))
412 audioFile.setFileGenre(value);
414 else if(IS_KEY("Gen_Released_Date") || IS_KEY("Gen_Recorded_Date"))
416 unsigned int tmp = parseYear(value);
417 if(tmp > 0) audioFile.setFileYear(tmp);
419 else if(IS_KEY("Gen_Comment"))
421 audioFile.setFileComment(value);
423 else if(IS_KEY("Gen_Track/Position"))
425 bool ok = false;
426 unsigned int tmp = value.toUInt(&ok);
427 if(ok) audioFile.setFilePosition(tmp);
429 else if(IS_KEY("Gen_Cover") || IS_KEY("Gen_Cover_Type"))
431 if(*coverType == coverNone)
433 *coverType = coverJpeg;
436 else if(IS_KEY("Gen_Cover_Mime"))
438 QString temp = FIRST_TOK(value);
439 if(!temp.compare("image/jpeg", Qt::CaseInsensitive)) *coverType = coverJpeg;
440 else if(!temp.compare("image/png", Qt::CaseInsensitive)) *coverType = coverPng;
441 else if(!temp.compare("image/gif", Qt::CaseInsensitive)) *coverType = coverGif;
443 else if(IS_KEY("Gen_Cover_Data"))
445 if(!coverData->isEmpty()) coverData->clear();
446 coverData->append(QByteArray::fromBase64(FIRST_TOK(value).toLatin1()));
448 else
450 qWarning("Unknown key '%s' with value '%s' found!", key.toUtf8().constData(), value.toUtf8().constData());
452 return;
455 /*Audio Section*/
456 if(IS_SEC("Aud"))
459 if(IS_KEY("Aud_Format"))
461 audioFile.setFormatAudioType(value);
463 else if(IS_KEY("Aud_Format_Profile"))
465 audioFile.setFormatAudioProfile(value);
467 else if(IS_KEY("Aud_Format_Version"))
469 audioFile.setFormatAudioVersion(value);
471 else if(IS_KEY("Aud_Channel(s)"))
473 bool ok = false;
474 unsigned int tmp = value.toUInt(&ok);
475 if(ok) audioFile.setFormatAudioChannels(tmp);
477 else if(IS_KEY("Aud_SamplingRate"))
479 bool ok = false;
480 unsigned int tmp = value.toUInt(&ok);
481 if(ok) audioFile.setFormatAudioSamplerate(tmp);
483 else if(IS_KEY("Aud_BitDepth"))
485 bool ok = false;
486 unsigned int tmp = value.toUInt(&ok);
487 if(ok) audioFile.setFormatAudioBitdepth(tmp);
489 else if(IS_KEY("Aud_Duration"))
491 unsigned int tmp = parseDuration(value);
492 if(tmp > 0) audioFile.setFileDuration(tmp);
494 else if(IS_KEY("Aud_BitRate"))
496 bool ok = false;
497 unsigned int tmp = value.toUInt(&ok);
498 if(ok) audioFile.setFormatAudioBitrate(tmp/1000);
500 else if(IS_KEY("Aud_BitRate_Mode"))
502 if(!value.compare("CBR", Qt::CaseInsensitive)) audioFile.setFormatAudioBitrateMode(AudioFileModel::BitrateModeConstant);
503 if(!value.compare("VBR", Qt::CaseInsensitive)) audioFile.setFormatAudioBitrateMode(AudioFileModel::BitrateModeVariable);
505 else
507 qWarning("Unknown key '%s' with value '%s' found!", key.toUtf8().constData(), value.toUtf8().constData());
509 return;
512 /*Section not recognized*/
513 qWarning("Unknown section: %s", key.toUtf8().constData());
516 bool AnalyzeTask::checkFile_CDDA(QFile &file)
518 file.reset();
519 QByteArray data = file.read(128);
521 int i = data.indexOf("RIFF");
522 int j = data.indexOf("CDDA");
523 int k = data.indexOf("fmt ");
525 return ((i >= 0) && (j >= 0) && (k >= 0) && (k > j) && (j > i));
528 void AnalyzeTask::retrieveCover(AudioFileModel &audioFile, cover_t coverType, const QByteArray &coverData)
530 qDebug("Retrieving cover!");
531 QString extension;
533 switch(coverType)
535 case coverPng:
536 extension = QString::fromLatin1("png");
537 break;
538 case coverGif:
539 extension = QString::fromLatin1("gif");
540 break;
541 default:
542 extension = QString::fromLatin1("jpg");
543 break;
546 if(!(QImage::fromData(coverData, extension.toUpper().toLatin1().constData()).isNull()))
548 QFile coverFile(QString("%1/%2.%3").arg(lamexp_temp_folder2(), lamexp_rand_str(), extension));
549 if(coverFile.open(QIODevice::WriteOnly))
551 coverFile.write(coverData);
552 coverFile.close();
553 audioFile.setFileCover(coverFile.fileName(), true);
556 else
558 qWarning("Image data seems to be invalid :-(");
562 bool AnalyzeTask::analyzeAvisynthFile(const QString &filePath, AudioFileModel &info)
564 QProcess process;
565 process.setProcessChannelMode(QProcess::MergedChannels);
566 process.setReadChannel(QProcess::StandardOutput);
567 process.start(m_avs2wavBin, QStringList() << QDir::toNativeSeparators(filePath) << "?");
569 if(!process.waitForStarted())
571 qWarning("AVS2WAV process failed to create!");
572 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
573 process.kill();
574 process.waitForFinished(-1);
575 return false;
578 bool bInfoHeaderFound = false;
580 while(process.state() != QProcess::NotRunning)
582 if(*m_abortFlag)
584 process.kill();
585 qWarning("Process was aborted on user request!");
586 break;
589 if(!process.waitForReadyRead())
591 if(process.state() == QProcess::Running)
593 qWarning("AVS2WAV time out. Killing process and skipping file!");
594 process.kill();
595 process.waitForFinished(-1);
596 return false;
600 QByteArray data;
602 while(process.canReadLine())
604 QString line = QString::fromUtf8(process.readLine().constData()).simplified();
605 if(!line.isEmpty())
607 int index = line.indexOf(':');
608 if(index > 0)
610 QString key = line.left(index).trimmed();
611 QString val = line.mid(index+1).trimmed();
613 if(bInfoHeaderFound && !key.isEmpty() && !val.isEmpty())
615 if(key.compare("TotalSeconds", Qt::CaseInsensitive) == 0)
617 bool ok = false;
618 unsigned int duration = val.toUInt(&ok);
619 if(ok) info.setFileDuration(duration);
621 if(key.compare("SamplesPerSec", Qt::CaseInsensitive) == 0)
623 bool ok = false;
624 unsigned int samplerate = val.toUInt(&ok);
625 if(ok) info.setFormatAudioSamplerate (samplerate);
627 if(key.compare("Channels", Qt::CaseInsensitive) == 0)
629 bool ok = false;
630 unsigned int channels = val.toUInt(&ok);
631 if(ok) info.setFormatAudioChannels(channels);
633 if(key.compare("BitsPerSample", Qt::CaseInsensitive) == 0)
635 bool ok = false;
636 unsigned int bitdepth = val.toUInt(&ok);
637 if(ok) info.setFormatAudioBitdepth(bitdepth);
641 else
643 if(line.contains("[Audio Info]", Qt::CaseInsensitive))
645 info.setFormatAudioType("Avisynth");
646 info.setFormatContainerType("Avisynth");
647 bInfoHeaderFound = true;
654 process.waitForFinished();
655 if(process.state() != QProcess::NotRunning)
657 process.kill();
658 process.waitForFinished(-1);
661 //Check exit code
662 switch(process.exitCode())
664 case 0:
665 qDebug("Avisynth script was analyzed successfully.");
666 return true;
667 break;
668 case -5:
669 qWarning("It appears that Avisynth is not installed on the system!");
670 return false;
671 break;
672 default:
673 qWarning("Failed to open the Avisynth script, bad AVS file?");
674 return false;
675 break;
679 unsigned int AnalyzeTask::parseYear(const QString &str)
681 if(str.startsWith("UTC", Qt::CaseInsensitive))
683 QDate date = QDate::fromString(str.mid(3).trimmed().left(10), "yyyy-MM-dd");
684 if(date.isValid())
686 return date.year();
688 else
690 return 0;
693 else
695 bool ok = false;
696 int year = str.toInt(&ok);
697 if(ok && year > 0)
699 return year;
701 else
703 return 0;
708 unsigned int AnalyzeTask::parseDuration(const QString &str)
710 bool ok = false;
711 unsigned int value = str.toUInt(&ok);
712 return ok ? (value/1000) : 0;
715 unsigned __int64 AnalyzeTask::makeThreadIdx(void)
717 s_waitMutex.lock();
718 unsigned __int64 idx = s_threadIdx_created++;
719 s_waitMutex.unlock();
721 return idx;
724 void AnalyzeTask::waitForPreviousThreads(void)
726 //This function will block until all threads with a *lower* index have terminated.
727 //Required to make sure that the files will be added in the "correct" order!
729 for(int i = 0; i < 64; i++)
731 s_waitMutex.lock();
733 if((s_threadIdx_finished >= m_threadIdx) || *m_abortFlag)
735 s_waitMutex.unlock();
736 break;
739 if(!s_waitCond.wait(&s_waitMutex, 1250))
741 qWarning("FileAnalyzerTask: Timeout, retrying!");
744 s_waitMutex.unlock();
748 ////////////////////////////////////////////////////////////
749 // Public Functions
750 ////////////////////////////////////////////////////////////
752 unsigned int AnalyzeTask::filesAccepted(void)
754 QReadLocker lock(&s_lock);
755 return s_filesAccepted;
758 unsigned int AnalyzeTask::filesRejected(void)
760 QReadLocker lock(&s_lock);
761 return s_filesRejected;
764 unsigned int AnalyzeTask::filesDenied(void)
766 QReadLocker lock(&s_lock);
767 return s_filesDenied;
770 unsigned int AnalyzeTask::filesDummyCDDA(void)
772 QReadLocker lock(&s_lock);
773 return s_filesDummyCDDA;
776 unsigned int AnalyzeTask::filesCueSheet(void)
778 QReadLocker lock(&s_lock);
779 return s_filesCueSheet;
782 int AnalyzeTask::getAdditionalFiles(QStringList &fileList)
784 QReadLocker readLock(&s_lock);
785 int count = s_additionalFiles.count();
786 readLock.unlock();
788 if(count > 0)
790 QWriteLocker lock(&s_lock);
791 count = s_additionalFiles.count();
792 fileList << s_additionalFiles;
793 s_additionalFiles.clear();
794 return count;
797 return 0;
800 bool AnalyzeTask::waitForOneThread(unsigned long timeout)
802 bool ret = false;
804 s_waitMutex.lock();
805 ret = s_waitCond.wait(&s_waitMutex, timeout);
806 s_waitMutex.unlock();
808 return ret;
811 void AnalyzeTask::reset(void)
813 QWriteLocker lock(&s_lock);
814 s_filesAccepted = 0;
815 s_filesRejected = 0;
816 s_filesDenied = 0;
817 s_filesDummyCDDA = 0;
818 s_filesCueSheet = 0;
819 s_additionalFiles.clear();
820 s_recentlyAdded.clear();
821 lock.unlock();
823 s_waitMutex.lock();
824 s_threadIdx_created = 0;
825 s_threadIdx_finished = 0;
826 s_waitMutex.unlock();
828 ////////////////////////////////////////////////////////////
829 // EVENTS
830 ////////////////////////////////////////////////////////////
832 /*NONE*/