1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2012 LoRd_MuldeR <MuldeR2@GMX.de>
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.
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.h"
25 #include "LockedFile.h"
26 #include "Model_AudioFile.h"
27 #include "PlaylistImporter.h"
39 ////////////////////////////////////////////////////////////
41 ////////////////////////////////////////////////////////////
43 FileAnalyzer::FileAnalyzer(const QStringList &inputFiles)
45 m_inputFiles(inputFiles),
46 m_mediaInfoBin(lamexp_lookup_tool("mediainfo.exe")),
47 m_avs2wavBin(lamexp_lookup_tool("avs2wav.exe")),
53 if(m_mediaInfoBin.isEmpty())
55 qFatal("Invalid path to MediaInfo binary. Tool not initialized properly.");
65 ////////////////////////////////////////////////////////////
67 ////////////////////////////////////////////////////////////
69 void FileAnalyzer::run()
81 m_recentlyAdded.clear();
84 while(!m_inputFiles.isEmpty())
86 int fileType = fileTypeNormal;
87 QString currentFile = QDir::fromNativeSeparators(m_inputFiles.takeFirst());
88 qDebug("Analyzing: %s", currentFile.toUtf8().constData());
89 emit fileSelected(QFileInfo(currentFile).fileName());
90 AudioFileModel file = analyzeFile(currentFile, &fileType);
94 MessageBeep(MB_ICONERROR);
96 qWarning("Operation cancelled by user!");
99 if(fileType == fileTypeSkip)
101 qWarning("File was recently added, skipping!");
104 if(fileType == fileTypeDenied)
107 qWarning("Cannot access file for reading, skipping!");
110 if(fileType == fileTypeCDDA)
113 qWarning("Dummy CDDA file detected, skipping!");
117 if(file.fileName().isEmpty() || file.formatContainerType().isEmpty() || file.formatAudioType().isEmpty())
119 if(PlaylistImporter::importPlaylist(m_inputFiles, currentFile))
121 qDebug("Imported playlist file.");
123 else if(!QFileInfo(currentFile).suffix().compare("cue", Qt::CaseInsensitive))
125 qWarning("Cue Sheet file detected, skipping!");
128 else if(!QFileInfo(currentFile).suffix().compare("avs", Qt::CaseInsensitive))
130 qDebug("Found a potential Avisynth script, investigating...");
131 if(analyzeAvisynthFile(currentFile, file))
134 emit fileAnalyzed(file);
138 qDebug("Rejected Avisynth file: %s", file.filePath().toUtf8().constData());
144 qDebug("Rejected file of unknown type: %s", file.filePath().toUtf8().constData());
151 m_recentlyAdded.append(file.filePath());
152 emit fileAnalyzed(file);
155 qDebug("All files added.\n");
159 ////////////////////////////////////////////////////////////
161 ////////////////////////////////////////////////////////////
163 const AudioFileModel FileAnalyzer::analyzeFile(const QString &filePath, int *type)
165 *type = fileTypeNormal;
167 AudioFileModel audioFile(filePath);
168 m_currentSection = sectionOther;
169 m_currentCover = coverNone;
171 if(m_recentlyAdded.contains(filePath, Qt::CaseInsensitive))
173 *type = fileTypeSkip;
177 QFile readTest(filePath);
178 if(!readTest.open(QIODevice::ReadOnly))
180 *type = fileTypeDenied;
184 if(checkFile_CDDA(readTest))
186 *type = fileTypeCDDA;
193 process.setProcessChannelMode(QProcess::MergedChannels);
194 process.setReadChannel(QProcess::StandardOutput);
195 process.start(m_mediaInfoBin, QStringList() << QDir::toNativeSeparators(filePath));
197 if(!process.waitForStarted())
199 qWarning("MediaInfo process failed to create!");
200 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
202 process.waitForFinished(-1);
206 while(process.state() != QProcess::NotRunning)
211 qWarning("Process was aborted on user request!");
215 if(!process.waitForReadyRead())
217 if(process.state() == QProcess::Running)
219 qWarning("MediaInfo time out. Killing process and skipping file!");
221 process.waitForFinished(-1);
228 while(process.canReadLine())
230 QString line = QString::fromUtf8(process.readLine().constData()).simplified();
233 int index = line.indexOf(':');
236 QString key = line.left(index-1).trimmed();
237 QString val = line.mid(index+1).trimmed();
238 if(!key.isEmpty() && !val.isEmpty())
240 updateInfo(audioFile, key, val);
251 if(audioFile.fileName().isEmpty())
253 QString baseName = QFileInfo(filePath).fileName();
254 int index = baseName.lastIndexOf(".");
258 baseName = baseName.left(index);
261 baseName = baseName.replace("_", " ").simplified();
262 index = baseName.lastIndexOf(" - ");
266 baseName = baseName.mid(index + 3).trimmed();
269 audioFile.setFileName(baseName);
272 process.waitForFinished();
273 if(process.state() != QProcess::NotRunning)
276 process.waitForFinished(-1);
279 if(m_currentCover != coverNone)
281 retrieveCover(audioFile, filePath);
287 void FileAnalyzer::updateSection(const QString §ion)
289 if(section.startsWith("General", Qt::CaseInsensitive))
291 m_currentSection = sectionGeneral;
293 else if(!section.compare("Audio", Qt::CaseInsensitive) || section.startsWith("Audio #1", Qt::CaseInsensitive))
295 m_currentSection = sectionAudio;
297 else if(section.startsWith("Audio", Qt::CaseInsensitive) || section.startsWith("Video", Qt::CaseInsensitive) || section.startsWith("Text", Qt::CaseInsensitive) ||
298 section.startsWith("Menu", Qt::CaseInsensitive) || section.startsWith("Image", Qt::CaseInsensitive) || section.startsWith("Chapters", Qt::CaseInsensitive))
300 m_currentSection = sectionOther;
304 m_currentSection = sectionOther;
305 qWarning("Unknown section: %s", section.toUtf8().constData());
309 void FileAnalyzer::updateInfo(AudioFileModel &audioFile, const QString &key, const QString &value)
311 switch(m_currentSection)
314 if(!key.compare("Title", Qt::CaseInsensitive) || !key.compare("Track", Qt::CaseInsensitive) || !key.compare("Track Name", Qt::CaseInsensitive))
316 if(audioFile.fileName().isEmpty()) audioFile.setFileName(value);
318 else if(!key.compare("Duration", Qt::CaseInsensitive))
320 if(!audioFile.fileDuration()) audioFile.setFileDuration(parseDuration(value));
322 else if(!key.compare("Artist", Qt::CaseInsensitive) || !key.compare("Performer", Qt::CaseInsensitive))
324 if(audioFile.fileArtist().isEmpty()) audioFile.setFileArtist(value);
326 else if(!key.compare("Album", Qt::CaseInsensitive))
328 if(audioFile.fileAlbum().isEmpty()) audioFile.setFileAlbum(value);
330 else if(!key.compare("Genre", Qt::CaseInsensitive))
332 if(audioFile.fileGenre().isEmpty()) audioFile.setFileGenre(value);
334 else if(!key.compare("Year", Qt::CaseInsensitive) || !key.compare("Recorded Date", Qt::CaseInsensitive) || !key.compare("Encoded Date", Qt::CaseInsensitive))
336 if(!audioFile.fileYear()) audioFile.setFileYear(parseYear(value));
338 else if(!key.compare("Comment", Qt::CaseInsensitive))
340 if(audioFile.fileComment().isEmpty()) audioFile.setFileComment(value);
342 else if(!key.compare("Track Name/Position", Qt::CaseInsensitive))
344 if(!audioFile.filePosition()) audioFile.setFilePosition(value.toInt());
346 else if(!key.compare("Format", Qt::CaseInsensitive))
348 if(audioFile.formatContainerType().isEmpty()) audioFile.setFormatContainerType(value);
350 else if(!key.compare("Format Profile", Qt::CaseInsensitive))
352 if(audioFile.formatContainerProfile().isEmpty()) audioFile.setFormatContainerProfile(value);
354 else if(!key.compare("Cover", Qt::CaseInsensitive) || !key.compare("Cover type", Qt::CaseInsensitive))
356 if(m_currentCover == coverNone) m_currentCover = coverJpeg;
358 else if(!key.compare("Cover MIME", Qt::CaseInsensitive))
360 QString temp = value.split(" ", QString::SkipEmptyParts, Qt::CaseInsensitive).first();
361 if(!temp.compare("image/jpeg", Qt::CaseInsensitive))
363 m_currentCover = coverJpeg;
365 else if(!temp.compare("image/png", Qt::CaseInsensitive))
367 m_currentCover = coverPng;
369 else if(!temp.compare("image/gif", Qt::CaseInsensitive))
371 m_currentCover = coverGif;
377 if(!key.compare("Year", Qt::CaseInsensitive) || !key.compare("Recorded Date", Qt::CaseInsensitive) || !key.compare("Encoded Date", Qt::CaseInsensitive))
379 if(!audioFile.fileYear()) audioFile.setFileYear(parseYear(value));
381 else if(!key.compare("Format", Qt::CaseInsensitive))
383 if(audioFile.formatAudioType().isEmpty()) audioFile.setFormatAudioType(value);
385 else if(!key.compare("Format Profile", Qt::CaseInsensitive))
387 if(audioFile.formatAudioProfile().isEmpty()) audioFile.setFormatAudioProfile(value);
389 else if(!key.compare("Format Version", Qt::CaseInsensitive))
391 if(audioFile.formatAudioVersion().isEmpty()) audioFile.setFormatAudioVersion(value);
393 else if(!key.compare("Channel(s)", Qt::CaseInsensitive))
395 if(!audioFile.formatAudioChannels()) audioFile.setFormatAudioChannels(value.split(" ", QString::SkipEmptyParts).first().toUInt());
397 else if(!key.compare("Sampling rate", Qt::CaseInsensitive))
399 if(!audioFile.formatAudioSamplerate())
402 float fTemp = abs(value.split(" ", QString::SkipEmptyParts).first().toFloat(&ok));
403 if(ok) audioFile.setFormatAudioSamplerate(static_cast<unsigned int>(floor(fTemp * 1000.0f + 0.5f)));
406 else if(!key.compare("Bit depth", Qt::CaseInsensitive))
408 if(!audioFile.formatAudioBitdepth()) audioFile.setFormatAudioBitdepth(value.split(" ", QString::SkipEmptyParts).first().toUInt());
410 else if(!key.compare("Duration", Qt::CaseInsensitive))
412 if(!audioFile.fileDuration()) audioFile.setFileDuration(parseDuration(value));
414 else if(!key.compare("Bit rate", Qt::CaseInsensitive))
416 if(!audioFile.formatAudioBitrate())
419 unsigned int uiTemp = value.split(" ", QString::SkipEmptyParts).first().toUInt(&ok);
422 audioFile.setFormatAudioBitrate(uiTemp);
426 float fTemp = abs(value.split(" ", QString::SkipEmptyParts).first().toFloat(&ok));
427 if(ok) audioFile.setFormatAudioBitrate(static_cast<unsigned int>(floor(fTemp + 0.5f)));
431 else if(!key.compare("Bit rate mode", Qt::CaseInsensitive))
433 if(audioFile.formatAudioBitrateMode() == AudioFileModel::BitrateModeUndefined)
435 if(!value.compare("Constant", Qt::CaseInsensitive)) audioFile.setFormatAudioBitrateMode(AudioFileModel::BitrateModeConstant);
436 if(!value.compare("Variable", Qt::CaseInsensitive)) audioFile.setFormatAudioBitrateMode(AudioFileModel::BitrateModeVariable);
443 unsigned int FileAnalyzer::parseYear(const QString &str)
445 if(str.startsWith("UTC", Qt::CaseInsensitive))
447 QDate date = QDate::fromString(str.mid(3).trimmed().left(10), "yyyy-MM-dd");
460 int year = str.toInt(&ok);
472 unsigned int FileAnalyzer::parseDuration(const QString &str)
476 time = QTime::fromString(str, "z'ms'");
479 return qMax(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
482 time = QTime::fromString(str, "s's 'z'ms'");
485 return qMax(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
488 time = QTime::fromString(str, "m'mn 's's'");
491 return qMax(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
494 time = QTime::fromString(str, "h'h 'm'mn'");
497 return qMax(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
503 bool FileAnalyzer::checkFile_CDDA(QFile &file)
506 QByteArray data = file.read(128);
508 int i = data.indexOf("RIFF");
509 int j = data.indexOf("CDDA");
510 int k = data.indexOf("fmt ");
512 return ((i >= 0) && (j >= 0) && (k >= 0) && (k > j) && (j > i));
515 void FileAnalyzer::retrieveCover(AudioFileModel &audioFile, const QString &filePath)
517 qDebug("Retrieving cover from: %s", filePath.toUtf8().constData());
520 switch(m_currentCover)
523 extension = QString::fromLatin1("png");
526 extension = QString::fromLatin1("gif");
529 extension = QString::fromLatin1("jpg");
534 process.setProcessChannelMode(QProcess::MergedChannels);
535 process.setReadChannel(QProcess::StandardOutput);
536 process.start(m_mediaInfoBin, QStringList() << "-f" << QDir::toNativeSeparators(filePath));
538 if(!process.waitForStarted())
540 qWarning("MediaInfo process failed to create!");
541 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
543 process.waitForFinished(-1);
547 while(process.state() != QProcess::NotRunning)
552 qWarning("Process was aborted on user request!");
556 if(!process.waitForReadyRead())
558 if(process.state() == QProcess::Running)
560 qWarning("MediaInfo time out. Killing process and skipping file!");
562 process.waitForFinished(-1);
567 while(process.canReadLine())
569 QString line = QString::fromUtf8(process.readLine().constData()).simplified();
572 int index = line.indexOf(':');
575 QString key = line.left(index-1).trimmed();
576 QString val = line.mid(index+1).trimmed();
577 if(!key.isEmpty() && !val.isEmpty())
579 if(!key.compare("Cover_Data", Qt::CaseInsensitive))
581 if(val.indexOf(" ") > 0)
583 val = val.split(" ", QString::SkipEmptyParts, Qt::CaseInsensitive).first();
585 QByteArray coverData = QByteArray::fromBase64(val.toLatin1());
586 if(!(QImage::fromData(coverData, extension.toUpper().toLatin1().constData()).isNull()))
588 QFile coverFile(QString("%1/%2.%3").arg(lamexp_temp_folder2(), lamexp_rand_str(), extension));
589 if(coverFile.open(QIODevice::WriteOnly))
591 coverFile.write(coverData);
593 audioFile.setFileCover(coverFile.fileName(), true);
598 qWarning("Image data seems to be invalid :-(");
608 process.waitForFinished();
609 if(process.state() != QProcess::NotRunning)
612 process.waitForFinished(-1);
616 bool FileAnalyzer::analyzeAvisynthFile(const QString &filePath, AudioFileModel &info)
619 process.setProcessChannelMode(QProcess::MergedChannels);
620 process.setReadChannel(QProcess::StandardOutput);
621 process.start(m_avs2wavBin, QStringList() << QDir::toNativeSeparators(filePath) << "?");
623 if(!process.waitForStarted())
625 qWarning("AVS2WAV process failed to create!");
626 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
628 process.waitForFinished(-1);
632 bool bInfoHeaderFound = false;
634 while(process.state() != QProcess::NotRunning)
639 qWarning("Process was aborted on user request!");
643 if(!process.waitForReadyRead())
645 if(process.state() == QProcess::Running)
647 qWarning("AVS2WAV time out. Killing process and skipping file!");
649 process.waitForFinished(-1);
656 while(process.canReadLine())
658 QString line = QString::fromUtf8(process.readLine().constData()).simplified();
661 int index = line.indexOf(':');
664 QString key = line.left(index).trimmed();
665 QString val = line.mid(index+1).trimmed();
667 if(bInfoHeaderFound && !key.isEmpty() && !val.isEmpty())
669 if(key.compare("TotalSeconds", Qt::CaseInsensitive) == 0)
672 unsigned int duration = val.toUInt(&ok);
673 if(ok) info.setFileDuration(duration);
675 if(key.compare("SamplesPerSec", Qt::CaseInsensitive) == 0)
678 unsigned int samplerate = val.toUInt(&ok);
679 if(ok) info.setFormatAudioSamplerate (samplerate);
681 if(key.compare("Channels", Qt::CaseInsensitive) == 0)
684 unsigned int channels = val.toUInt(&ok);
685 if(ok) info.setFormatAudioChannels(channels);
687 if(key.compare("BitsPerSample", Qt::CaseInsensitive) == 0)
690 unsigned int bitdepth = val.toUInt(&ok);
691 if(ok) info.setFormatAudioBitdepth(bitdepth);
697 if(line.contains("[Audio Info]", Qt::CaseInsensitive))
699 info.setFormatAudioType("Avisynth");
700 info.setFormatContainerType("Avisynth");
701 bInfoHeaderFound = true;
708 process.waitForFinished();
709 if(process.state() != QProcess::NotRunning)
712 process.waitForFinished(-1);
716 switch(process.exitCode())
719 qDebug("Avisynth script was analyzed successfully.");
723 qWarning("It appears that Avisynth is not installed on the system!");
727 qWarning("Failed to open the Avisynth script, bad AVS file?");
733 ////////////////////////////////////////////////////////////
735 ////////////////////////////////////////////////////////////
737 unsigned int FileAnalyzer::filesAccepted(void)
739 return m_filesAccepted;
742 unsigned int FileAnalyzer::filesRejected(void)
744 return m_filesRejected;
747 unsigned int FileAnalyzer::filesDenied(void)
749 return m_filesDenied;
752 unsigned int FileAnalyzer::filesDummyCDDA(void)
754 return m_filesDummyCDDA;
757 unsigned int FileAnalyzer::filesCueSheet(void)
759 return m_filesCueSheet;
762 ////////////////////////////////////////////////////////////
764 ////////////////////////////////////////////////////////////