Updated avs2wav tool.
[LameXP.git] / src / Thread_FileAnalyzer.cpp
blob18d89d96bdfd6f2a1493ec5d72ca81e2320b0578
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2011 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.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>
36 #include <math.h>
38 ////////////////////////////////////////////////////////////
39 // Constructor
40 ////////////////////////////////////////////////////////////
42 FileAnalyzer::FileAnalyzer(const QStringList &inputFiles)
44 m_inputFiles(inputFiles),
45 m_mediaInfoBin(lamexp_lookup_tool("mediainfo.exe")),
46 m_abortFlag(false)
48 m_bSuccess = false;
49 m_bAborted = false;
51 if(m_mediaInfoBin.isEmpty())
53 qFatal("Invalid path to MediaInfo binary. Tool not initialized properly.");
56 m_filesAccepted = 0;
57 m_filesRejected = 0;
58 m_filesDenied = 0;
59 m_filesDummyCDDA = 0;
60 m_filesCueSheet = 0;
63 ////////////////////////////////////////////////////////////
64 // Thread Main
65 ////////////////////////////////////////////////////////////
67 void FileAnalyzer::run()
69 m_bSuccess = false;
70 m_bAborted = false;
72 m_filesAccepted = 0;
73 m_filesRejected = 0;
74 m_filesDenied = 0;
75 m_filesDummyCDDA = 0;
76 m_filesCueSheet = 0;
78 m_inputFiles.sort();
80 m_abortFlag = false;
82 while(!m_inputFiles.isEmpty())
84 int fileType = fileTypeNormal;
85 QString currentFile = QDir::fromNativeSeparators(m_inputFiles.takeFirst());
86 qDebug64("Analyzing: %1", currentFile);
87 emit fileSelected(QFileInfo(currentFile).fileName());
88 AudioFileModel file = analyzeFile(currentFile, &fileType);
90 if(m_abortFlag)
92 MessageBeep(MB_ICONERROR);
93 m_bAborted = true;
94 qWarning("Operation cancelled by user!");
95 return;
98 if(fileType == fileTypeDenied)
100 m_filesDenied++;
101 qWarning("Cannot access file for reading, skipping!");
102 continue;
104 if(fileType == fileTypeCDDA)
106 m_filesDummyCDDA++;
107 qWarning("Dummy CDDA file detected, skipping!");
108 continue;
111 if(file.fileName().isEmpty() || file.formatContainerType().isEmpty() || file.formatAudioType().isEmpty())
113 if(PlaylistImporter::importPlaylist(m_inputFiles, currentFile))
115 qDebug("Imported playlist file.");
117 else if(!QFileInfo(currentFile).suffix().compare("cue", Qt::CaseInsensitive))
119 qWarning("Cue Sheet file detected, skipping!");
120 m_filesCueSheet++;
122 else if(!QFileInfo(currentFile).suffix().compare("avs", Qt::CaseInsensitive))
124 qWarning("Added an potential Avisynth script file!");
125 file.setFormatAudioType("Avisynth");
126 file.setFormatContainerType("Avisynth");
127 m_filesAccepted++;
128 emit fileAnalyzed(file);
130 else
132 qDebug64("Rejected file of unknown type: %1", file.filePath());
133 m_filesRejected++;
135 continue;
138 m_filesAccepted++;
139 emit fileAnalyzed(file);
142 qDebug("All files added.\n");
143 m_bSuccess = true;
146 ////////////////////////////////////////////////////////////
147 // Privtae Functions
148 ////////////////////////////////////////////////////////////
150 const AudioFileModel FileAnalyzer::analyzeFile(const QString &filePath, int *type)
152 *type = fileTypeNormal;
154 AudioFileModel audioFile(filePath);
155 m_currentSection = sectionOther;
156 m_currentCover = coverNone;
158 QFile readTest(filePath);
159 if(!readTest.open(QIODevice::ReadOnly))
161 *type = fileTypeDenied;
162 return audioFile;
165 if(checkFile_CDDA(readTest))
167 *type = fileTypeCDDA;
168 return audioFile;
171 readTest.close();
173 QProcess process;
174 process.setProcessChannelMode(QProcess::MergedChannels);
175 process.setReadChannel(QProcess::StandardOutput);
176 process.start(m_mediaInfoBin, QStringList() << QDir::toNativeSeparators(filePath));
178 if(!process.waitForStarted())
180 qWarning("MediaInfo process failed to create!");
181 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
182 process.kill();
183 process.waitForFinished(-1);
184 return audioFile;
187 while(process.state() != QProcess::NotRunning)
189 if(m_abortFlag)
191 process.kill();
192 qWarning("Process was aborted on user request!");
193 break;
196 if(!process.waitForReadyRead())
198 if(process.state() == QProcess::Running)
200 qWarning("MediaInfo time out. Killing process and skipping file!");
201 process.kill();
202 process.waitForFinished(-1);
203 return audioFile;
207 QByteArray data;
209 while(process.canReadLine())
211 QString line = QString::fromUtf8(process.readLine().constData()).simplified();
212 if(!line.isEmpty())
214 int index = line.indexOf(':');
215 if(index > 0)
217 QString key = line.left(index-1).trimmed();
218 QString val = line.mid(index+1).trimmed();
219 if(!key.isEmpty() && !val.isEmpty())
221 updateInfo(audioFile, key, val);
224 else
226 updateSection(line);
232 if(audioFile.fileName().isEmpty())
234 QString baseName = QFileInfo(filePath).fileName();
235 int index = baseName.lastIndexOf(".");
237 if(index >= 0)
239 baseName = baseName.left(index);
242 baseName = baseName.replace("_", " ").simplified();
243 index = baseName.lastIndexOf(" - ");
245 if(index >= 0)
247 baseName = baseName.mid(index + 3).trimmed();
250 audioFile.setFileName(baseName);
253 if(m_currentCover != coverNone)
255 retrieveCover(audioFile, filePath);
258 return audioFile;
261 void FileAnalyzer::updateSection(const QString &section)
263 if(section.startsWith("General", Qt::CaseInsensitive))
265 m_currentSection = sectionGeneral;
267 else if(!section.compare("Audio", Qt::CaseInsensitive) || section.startsWith("Audio #1", Qt::CaseInsensitive))
269 m_currentSection = sectionAudio;
271 else if(section.startsWith("Audio", Qt::CaseInsensitive) || section.startsWith("Video", Qt::CaseInsensitive) || section.startsWith("Text", Qt::CaseInsensitive) ||
272 section.startsWith("Menu", Qt::CaseInsensitive) || section.startsWith("Image", Qt::CaseInsensitive) || section.startsWith("Chapters", Qt::CaseInsensitive))
274 m_currentSection = sectionOther;
276 else
278 m_currentSection = sectionOther;
279 qWarning("Unknown section: %s", section.toUtf8().constData());
283 void FileAnalyzer::updateInfo(AudioFileModel &audioFile, const QString &key, const QString &value)
285 switch(m_currentSection)
287 case sectionGeneral:
288 if(!key.compare("Title", Qt::CaseInsensitive) || !key.compare("Track", Qt::CaseInsensitive) || !key.compare("Track Name", Qt::CaseInsensitive))
290 if(audioFile.fileName().isEmpty()) audioFile.setFileName(value);
292 else if(!key.compare("Duration", Qt::CaseInsensitive))
294 if(!audioFile.fileDuration()) audioFile.setFileDuration(parseDuration(value));
296 else if(!key.compare("Artist", Qt::CaseInsensitive) || !key.compare("Performer", Qt::CaseInsensitive))
298 if(audioFile.fileArtist().isEmpty()) audioFile.setFileArtist(value);
300 else if(!key.compare("Album", Qt::CaseInsensitive))
302 if(audioFile.fileAlbum().isEmpty()) audioFile.setFileAlbum(value);
304 else if(!key.compare("Genre", Qt::CaseInsensitive))
306 if(audioFile.fileGenre().isEmpty()) audioFile.setFileGenre(value);
308 else if(!key.compare("Year", Qt::CaseInsensitive) || !key.compare("Recorded Date", Qt::CaseInsensitive) || !key.compare("Encoded Date", Qt::CaseInsensitive))
310 if(!audioFile.fileYear()) audioFile.setFileYear(parseYear(value));
312 else if(!key.compare("Comment", Qt::CaseInsensitive))
314 if(audioFile.fileComment().isEmpty()) audioFile.setFileComment(value);
316 else if(!key.compare("Track Name/Position", Qt::CaseInsensitive))
318 if(!audioFile.filePosition()) audioFile.setFilePosition(value.toInt());
320 else if(!key.compare("Format", Qt::CaseInsensitive))
322 if(audioFile.formatContainerType().isEmpty()) audioFile.setFormatContainerType(value);
324 else if(!key.compare("Format Profile", Qt::CaseInsensitive))
326 if(audioFile.formatContainerProfile().isEmpty()) audioFile.setFormatContainerProfile(value);
328 else if(!key.compare("Cover", Qt::CaseInsensitive) || !key.compare("Cover type", Qt::CaseInsensitive))
330 if(m_currentCover == coverNone) m_currentCover = coverJpeg;
332 else if(!key.compare("Cover MIME", Qt::CaseInsensitive))
334 QString temp = value.split(" ", QString::SkipEmptyParts, Qt::CaseInsensitive).first();
335 if(!temp.compare("image/jpeg", Qt::CaseInsensitive))
337 m_currentCover = coverJpeg;
339 else if(!temp.compare("image/png", Qt::CaseInsensitive))
341 m_currentCover = coverPng;
343 else if(!temp.compare("image/gif", Qt::CaseInsensitive))
345 m_currentCover = coverGif;
348 break;
350 case sectionAudio:
351 if(!key.compare("Year", Qt::CaseInsensitive) || !key.compare("Recorded Date", Qt::CaseInsensitive) || !key.compare("Encoded Date", Qt::CaseInsensitive))
353 if(!audioFile.fileYear()) audioFile.setFileYear(parseYear(value));
355 else if(!key.compare("Format", Qt::CaseInsensitive))
357 if(audioFile.formatAudioType().isEmpty()) audioFile.setFormatAudioType(value);
359 else if(!key.compare("Format Profile", Qt::CaseInsensitive))
361 if(audioFile.formatAudioProfile().isEmpty()) audioFile.setFormatAudioProfile(value);
363 else if(!key.compare("Format Version", Qt::CaseInsensitive))
365 if(audioFile.formatAudioVersion().isEmpty()) audioFile.setFormatAudioVersion(value);
367 else if(!key.compare("Channel(s)", Qt::CaseInsensitive))
369 if(!audioFile.formatAudioChannels()) audioFile.setFormatAudioChannels(value.split(" ", QString::SkipEmptyParts).first().toInt());
371 else if(!key.compare("Sampling rate", Qt::CaseInsensitive))
373 if(!audioFile.formatAudioSamplerate()) audioFile.setFormatAudioSamplerate(ceil(value.split(" ", QString::SkipEmptyParts).first().toFloat() * 1000.0f));
375 else if(!key.compare("Bit depth", Qt::CaseInsensitive))
377 if(!audioFile.formatAudioBitdepth()) audioFile.setFormatAudioBitdepth(value.split(" ", QString::SkipEmptyParts).first().toInt());
379 else if(!key.compare("Duration", Qt::CaseInsensitive))
381 if(!audioFile.fileDuration()) audioFile.setFileDuration(parseDuration(value));
383 break;
387 unsigned int FileAnalyzer::parseYear(const QString &str)
389 if(str.startsWith("UTC", Qt::CaseInsensitive))
391 QDate date = QDate::fromString(str.mid(3).trimmed().left(10), "yyyy-MM-dd");
392 if(date.isValid())
394 return date.year();
396 else
398 return 0;
401 else
403 bool ok = false;
404 int year = str.toInt(&ok);
405 if(ok && year > 0)
407 return year;
409 else
411 return 0;
416 unsigned int FileAnalyzer::parseDuration(const QString &str)
418 QTime time;
420 time = QTime::fromString(str, "z'ms'");
421 if(time.isValid())
423 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
426 time = QTime::fromString(str, "s's 'z'ms'");
427 if(time.isValid())
429 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
432 time = QTime::fromString(str, "m'mn 's's'");
433 if(time.isValid())
435 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
438 time = QTime::fromString(str, "h'h 'm'mn'");
439 if(time.isValid())
441 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
444 return 0;
447 bool FileAnalyzer::checkFile_CDDA(QFile &file)
449 file.reset();
450 QByteArray data = file.read(128);
452 int i = data.indexOf("RIFF");
453 int j = data.indexOf("CDDA");
454 int k = data.indexOf("fmt ");
456 return ((i >= 0) && (j >= 0) && (k >= 0) && (k > j) && (j > i));
459 void FileAnalyzer::retrieveCover(AudioFileModel &audioFile, const QString &filePath)
461 qDebug64("Retrieving cover from: %1", filePath);
462 QString extension;
464 switch(m_currentCover)
466 case coverPng:
467 extension = QString::fromLatin1("png");
468 break;
469 case coverGif:
470 extension = QString::fromLatin1("gif");
471 break;
472 default:
473 extension = QString::fromLatin1("jpg");
474 break;
477 QProcess process;
478 process.setProcessChannelMode(QProcess::MergedChannels);
479 process.setReadChannel(QProcess::StandardOutput);
480 process.start(m_mediaInfoBin, QStringList() << "-f" << QDir::toNativeSeparators(filePath));
482 if(!process.waitForStarted())
484 qWarning("MediaInfo process failed to create!");
485 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
486 process.kill();
487 process.waitForFinished(-1);
488 return;
491 while(process.state() != QProcess::NotRunning)
493 if(m_abortFlag)
495 process.kill();
496 qWarning("Process was aborted on user request!");
497 break;
500 if(!process.waitForReadyRead())
502 if(process.state() == QProcess::Running)
504 qWarning("MediaInfo time out. Killing process and skipping file!");
505 process.kill();
506 process.waitForFinished(-1);
507 return;
511 while(process.canReadLine())
513 QString line = QString::fromUtf8(process.readLine().constData()).simplified();
514 if(!line.isEmpty())
516 int index = line.indexOf(':');
517 if(index > 0)
519 QString key = line.left(index-1).trimmed();
520 QString val = line.mid(index+1).trimmed();
521 if(!key.isEmpty() && !val.isEmpty())
523 if(!key.compare("Cover_Data", Qt::CaseInsensitive))
525 if(val.indexOf(" ") > 0)
527 val = val.split(" ", QString::SkipEmptyParts, Qt::CaseInsensitive).first();
529 QByteArray coverData = QByteArray::fromBase64(val.toLatin1());
530 QFile coverFile(QString("%1/%2.%3").arg(lamexp_temp_folder2(), lamexp_rand_str(), extension));
531 if(coverFile.open(QIODevice::WriteOnly))
533 coverFile.write(coverData);
534 coverFile.close();
535 audioFile.setFileCover(coverFile.fileName(), true);
537 break;
546 ////////////////////////////////////////////////////////////
547 // Public Functions
548 ////////////////////////////////////////////////////////////
550 unsigned int FileAnalyzer::filesAccepted(void)
552 return m_filesAccepted;
555 unsigned int FileAnalyzer::filesRejected(void)
557 return m_filesRejected;
560 unsigned int FileAnalyzer::filesDenied(void)
562 return m_filesDenied;
565 unsigned int FileAnalyzer::filesDummyCDDA(void)
567 return m_filesDummyCDDA;
570 unsigned int FileAnalyzer::filesCueSheet(void)
572 return m_filesCueSheet;
575 ////////////////////////////////////////////////////////////
576 // EVENTS
577 ////////////////////////////////////////////////////////////
579 /*NONE*/