1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2011 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"
34 #include <QMessageBox>
38 //Un-escape XML characters
39 #define XML_DECODE replace("&", "&").replace("'", "'").replace(" ", " ").replace(""", "\"").replace("<", "<").replace(">", ">")
41 ////////////////////////////////////////////////////////////
43 ////////////////////////////////////////////////////////////
45 FileAnalyzer::FileAnalyzer(const QStringList
&inputFiles
)
47 m_inputFiles(inputFiles
),
48 m_mediaInfoBin_x86(lamexp_lookup_tool("mediainfo_i386.exe")),
49 m_mediaInfoBin_x64(lamexp_lookup_tool("mediainfo_x64.exe"))
53 if(m_mediaInfoBin_x86
.isEmpty() || m_mediaInfoBin_x64
.isEmpty())
55 qFatal("Invalid path to MediaInfo binary. Tool not initialized properly.");
63 ////////////////////////////////////////////////////////////
65 ////////////////////////////////////////////////////////////
67 void FileAnalyzer::run()
77 while(!m_inputFiles
.isEmpty())
79 QString currentFile
= QDir::fromNativeSeparators(m_inputFiles
.takeFirst());
80 qDebug("Analyzing: %s", currentFile
.toUtf8().constData());
81 emit
fileSelected(QFileInfo(currentFile
).fileName());
82 AudioFileModel file
= analyzeFile(currentFile
);
83 if(file
.fileName().isEmpty() || file
.formatContainerType().isEmpty() || file
.formatAudioType().isEmpty())
85 if(!importPlaylist(m_inputFiles
, currentFile
))
88 qDebug("Skipped: %s", file
.filePath().toUtf8().constData());
93 emit
fileAnalyzed(file
);
96 qDebug("All files added.\n");
100 ////////////////////////////////////////////////////////////
102 ////////////////////////////////////////////////////////////
104 const AudioFileModel
FileAnalyzer::analyzeFile(const QString
&filePath
)
106 lamexp_cpu_t cpuInfo
= lamexp_detect_cpu_features();
107 const QString mediaInfoBin
= cpuInfo
.x64
? m_mediaInfoBin_x64
: m_mediaInfoBin_x86
;
109 AudioFileModel
audioFile(filePath
);
110 m_currentSection
= sectionOther
;
112 QFile
readTest(filePath
);
113 if(!readTest
.open(QIODevice::ReadOnly
))
115 qWarning("Cannot access file for reading, skipping!");
125 process
.setProcessChannelMode(QProcess::MergedChannels
);
126 process
.setReadChannel(QProcess::StandardOutput
);
127 process
.start(mediaInfoBin
, QStringList() << QDir::toNativeSeparators(filePath
));
129 if(!process
.waitForStarted())
131 qWarning("MediaInfo process failed to create!");
132 qWarning("Error message: \"%s\"\n", process
.errorString().toLatin1().constData());
134 process
.waitForFinished(-1);
138 while(process
.state() != QProcess::NotRunning
)
140 if(!process
.waitForReadyRead())
142 if(process
.state() == QProcess::Running
)
144 qWarning("MediaInfo time out. Killing process and skipping file!");
146 process
.waitForFinished(-1);
153 while(process
.canReadLine())
155 QString line
= QString::fromUtf8(process
.readLine().constData()).simplified();
158 int index
= line
.indexOf(':');
161 QString key
= line
.left(index
-1).trimmed();
162 QString val
= line
.mid(index
+1).trimmed();
163 if(!key
.isEmpty() && !val
.isEmpty())
165 updateInfo(audioFile
, key
, val
);
176 if(audioFile
.fileName().isEmpty())
178 QString baseName
= QFileInfo(filePath
).fileName();
179 int index
= baseName
.lastIndexOf(".");
183 baseName
= baseName
.left(index
);
186 baseName
= baseName
.replace("_", " ").simplified();
187 index
= baseName
.lastIndexOf(" - ");
191 baseName
= baseName
.mid(index
+ 3).trimmed();
194 audioFile
.setFileName(baseName
);
200 void FileAnalyzer::updateSection(const QString
§ion
)
202 if(section
.startsWith("General", Qt::CaseInsensitive
))
204 m_currentSection
= sectionGeneral
;
206 else if(!section
.compare("Audio", Qt::CaseInsensitive
) || section
.startsWith("Audio #1", Qt::CaseInsensitive
))
208 m_currentSection
= sectionAudio
;
210 else if(section
.startsWith("Audio", Qt::CaseInsensitive
) || section
.startsWith("Video", Qt::CaseInsensitive
) || section
.startsWith("Text", Qt::CaseInsensitive
) ||
211 section
.startsWith("Menu", Qt::CaseInsensitive
) || section
.startsWith("Image", Qt::CaseInsensitive
) || section
.startsWith("Chapters", Qt::CaseInsensitive
))
213 m_currentSection
= sectionOther
;
217 qWarning("Unknown section: %s", section
.toUtf8().constData());
221 void FileAnalyzer::updateInfo(AudioFileModel
&audioFile
, const QString
&key
, const QString
&value
)
223 switch(m_currentSection
)
226 if(!key
.compare("Title", Qt::CaseInsensitive
) || !key
.compare("Track", Qt::CaseInsensitive
) || !key
.compare("Track Name", Qt::CaseInsensitive
))
228 if(audioFile
.fileName().isEmpty()) audioFile
.setFileName(value
);
230 else if(!key
.compare("Duration", Qt::CaseInsensitive
))
232 if(!audioFile
.fileDuration()) audioFile
.setFileDuration(parseDuration(value
));
234 else if(!key
.compare("Artist", Qt::CaseInsensitive
) || !key
.compare("Performer", Qt::CaseInsensitive
))
236 if(audioFile
.fileArtist().isEmpty()) audioFile
.setFileArtist(value
);
238 else if(!key
.compare("Album", Qt::CaseInsensitive
))
240 if(audioFile
.fileAlbum().isEmpty()) audioFile
.setFileAlbum(value
);
242 else if(!key
.compare("Genre", Qt::CaseInsensitive
))
244 if(audioFile
.fileGenre().isEmpty()) audioFile
.setFileGenre(value
);
246 else if(!key
.compare("Year", Qt::CaseInsensitive
) || !key
.compare("Recorded Date", Qt::CaseInsensitive
) || !key
.compare("Encoded Date", Qt::CaseInsensitive
))
248 if(!audioFile
.fileYear()) audioFile
.setFileYear(parseYear(value
));
250 else if(!key
.compare("Comment", Qt::CaseInsensitive
))
252 if(audioFile
.fileComment().isEmpty()) audioFile
.setFileComment(value
);
254 else if(!key
.compare("Track Name/Position", Qt::CaseInsensitive
))
256 if(!audioFile
.filePosition()) audioFile
.setFilePosition(value
.toInt());
258 else if(!key
.compare("Format", Qt::CaseInsensitive
))
260 if(audioFile
.formatContainerType().isEmpty()) audioFile
.setFormatContainerType(value
);
262 else if(!key
.compare("Format Profile", Qt::CaseInsensitive
))
264 if(audioFile
.formatContainerProfile().isEmpty()) audioFile
.setFormatContainerProfile(value
);
269 if(!key
.compare("Year", Qt::CaseInsensitive
) || !key
.compare("Recorded Date", Qt::CaseInsensitive
) || !key
.compare("Encoded Date", Qt::CaseInsensitive
))
271 if(!audioFile
.fileYear()) audioFile
.setFileYear(parseYear(value
));
273 else if(!key
.compare("Format", Qt::CaseInsensitive
))
275 if(audioFile
.formatAudioType().isEmpty()) audioFile
.setFormatAudioType(value
);
277 else if(!key
.compare("Format Profile", Qt::CaseInsensitive
))
279 if(audioFile
.formatAudioProfile().isEmpty()) audioFile
.setFormatAudioProfile(value
);
281 else if(!key
.compare("Format Version", Qt::CaseInsensitive
))
283 if(audioFile
.formatAudioVersion().isEmpty()) audioFile
.setFormatAudioVersion(value
);
285 else if(!key
.compare("Channel(s)", Qt::CaseInsensitive
))
287 if(!audioFile
.formatAudioChannels()) audioFile
.setFormatAudioChannels(value
.split(" ", QString::SkipEmptyParts
).first().toInt());
289 else if(!key
.compare("Sampling rate", Qt::CaseInsensitive
))
291 if(!audioFile
.formatAudioSamplerate()) audioFile
.setFormatAudioSamplerate(ceil(value
.split(" ", QString::SkipEmptyParts
).first().toFloat() * 1000.0f
));
293 else if(!key
.compare("Bit depth", Qt::CaseInsensitive
))
295 if(!audioFile
.formatAudioBitdepth()) audioFile
.setFormatAudioBitdepth(value
.split(" ", QString::SkipEmptyParts
).first().toInt());
297 else if(!key
.compare("Duration", Qt::CaseInsensitive
))
299 if(!audioFile
.fileDuration()) audioFile
.setFileDuration(parseDuration(value
));
305 unsigned int FileAnalyzer::parseYear(const QString
&str
)
307 if(str
.startsWith("UTC", Qt::CaseInsensitive
))
309 QDate date
= QDate::fromString(str
.mid(3).trimmed().left(10), "yyyy-MM-dd");
322 int year
= str
.toInt(&ok
);
334 unsigned int FileAnalyzer::parseDuration(const QString
&str
)
338 time
= QTime::fromString(str
, "z'ms'");
341 return max(1, (time
.hour() * 60 * 60) + (time
.minute() * 60) + time
.second());
344 time
= QTime::fromString(str
, "s's 'z'ms'");
347 return max(1, (time
.hour() * 60 * 60) + (time
.minute() * 60) + time
.second());
350 time
= QTime::fromString(str
, "m'mn 's's'");
353 return max(1, (time
.hour() * 60 * 60) + (time
.minute() * 60) + time
.second());
356 time
= QTime::fromString(str
, "h'h 'm'mn'");
359 return max(1, (time
.hour() * 60 * 60) + (time
.minute() * 60) + time
.second());
366 bool FileAnalyzer::importPlaylist(QStringList
&fileList
, const QString
&playlistFile
)
368 QFileInfo
file(playlistFile
);
369 QDir
baseDir(file
.canonicalPath());
371 QDir
rootDir(baseDir
);
372 while(rootDir
.cdUp());
375 if(file
.size() < 3 || file
.size() > 512000)
380 //Detect playlist type
381 playlist_t playlistType
= isPlaylist(file
.canonicalFilePath());
383 //Exit if not a playlist
384 if(playlistType
== noPlaylist
)
389 QFile
data(playlistFile
);
391 //Open file for reading
392 if(!data
.open(QIODevice::ReadOnly
))
397 //Parse playlist depending on type
401 return parsePlaylist_m3u(data
, fileList
, baseDir
, rootDir
);
404 return parsePlaylist_pls(data
, fileList
, baseDir
, rootDir
);
407 return parsePlaylist_wpl(data
, fileList
, baseDir
, rootDir
);
415 bool FileAnalyzer::parsePlaylist_m3u(QFile
&data
, QStringList
&fileList
, const QDir
&baseDir
, const QDir
&rootDir
)
417 QByteArray line
= data
.readLine();
419 while(line
.size() > 0)
421 QFileInfo
filename1(QDir::fromNativeSeparators(QString::fromUtf8(line
.constData(), line
.size()).trimmed()));
422 QFileInfo
filename2(QDir::fromNativeSeparators(QString::fromLatin1(line
.constData(), line
.size()).trimmed()));
424 filename1
.setCaching(false);
425 filename2
.setCaching(false);
427 if(!(filename1
.filePath().startsWith("#") || filename2
.filePath().startsWith("#")))
429 fixFilePath(filename1
, baseDir
, rootDir
);
430 fixFilePath(filename2
, baseDir
, rootDir
);
432 if(filename1
.exists())
434 if(isPlaylist(filename1
.canonicalFilePath()) == noPlaylist
)
436 fileList
<< filename1
.canonicalFilePath();
439 else if(filename2
.exists())
441 if(isPlaylist(filename2
.canonicalFilePath()) == noPlaylist
)
443 fileList
<< filename2
.canonicalFilePath();
448 line
= data
.readLine();
454 bool FileAnalyzer::parsePlaylist_pls(QFile
&data
, QStringList
&fileList
, const QDir
&baseDir
, const QDir
&rootDir
)
456 QRegExp
plsEntry("File(\\d+)=(.+)", Qt::CaseInsensitive
);
457 QByteArray line
= data
.readLine();
459 while(line
.size() > 0)
463 QString
temp1(QDir::fromNativeSeparators(QString::fromUtf8(line
.constData(), line
.size()).trimmed()));
464 QString
temp2(QDir::fromNativeSeparators(QString::fromLatin1(line
.constData(), line
.size()).trimmed()));
466 if(!flag
&& plsEntry
.indexIn(temp1
) >= 0)
468 QFileInfo
filename(QDir::fromNativeSeparators(plsEntry
.cap(2)).trimmed());
469 filename
.setCaching(false);
470 fixFilePath(filename
, baseDir
, rootDir
);
472 if(filename
.exists())
474 if(isPlaylist(filename
.canonicalFilePath()) == noPlaylist
)
476 fileList
<< filename
.canonicalFilePath();
482 if(!flag
&& plsEntry
.indexIn(temp2
) >= 0)
484 QFileInfo
filename(QDir::fromNativeSeparators(plsEntry
.cap(2)).trimmed());
485 filename
.setCaching(false);
486 fixFilePath(filename
, baseDir
, rootDir
);
488 if(filename
.exists())
490 if(isPlaylist(filename
.canonicalFilePath()) == noPlaylist
)
492 fileList
<< filename
.canonicalFilePath();
498 line
= data
.readLine();
504 bool FileAnalyzer::parsePlaylist_wpl(QFile
&data
, QStringList
&fileList
, const QDir
&baseDir
, const QDir
&rootDir
)
506 QRegExp
skipData("<!--(.+)-->", Qt::CaseInsensitive
);
507 QRegExp
wplEntry("<(media|ref)[^<>]*(src|href)=\"([^\"]+)\"[^<>]*>", Qt::CaseInsensitive
);
509 skipData
.setMinimal(true);
511 QByteArray buffer
= data
.readAll();
512 QString line
= QString::fromUtf8(buffer
.constData(), buffer
.size()).simplified();
517 while((index
= skipData
.indexIn(line
)) >= 0)
519 line
.remove(index
, skipData
.matchedLength());
524 while((offset
= wplEntry
.indexIn(line
, offset
) + 1) > 0)
526 QFileInfo
filename(QDir::fromNativeSeparators(wplEntry
.cap(3).XML_DECODE
.trimmed()));
527 filename
.setCaching(false);
528 fixFilePath(filename
, baseDir
, rootDir
);
530 if(filename
.exists())
532 if(isPlaylist(filename
.canonicalFilePath()) == noPlaylist
)
534 fileList
<< filename
.canonicalFilePath();
542 FileAnalyzer::playlist_t
FileAnalyzer::isPlaylist(const QString
&fileName
)
544 QFileInfo
file (fileName
);
546 if(file
.suffix().compare("m3u", Qt::CaseInsensitive
) == 0)
550 else if(file
.suffix().compare("m3u8", Qt::CaseInsensitive
) == 0)
554 else if(file
.suffix().compare("pls", Qt::CaseInsensitive
) == 0)
558 else if(file
.suffix().compare("asx", Qt::CaseInsensitive
) == 0)
562 else if(file
.suffix().compare("wpl", Qt::CaseInsensitive
) == 0)
572 void FileAnalyzer::fixFilePath(QFileInfo
&filename
, const QDir
&baseDir
, const QDir
&rootDir
)
574 if(filename
.filePath().startsWith("/"))
576 while(filename
.filePath().startsWith("/"))
578 filename
.setFile(filename
.filePath().mid(1));
580 filename
.setFile(rootDir
.filePath(filename
.filePath()));
583 if(!filename
.isAbsolute())
585 filename
.setFile(baseDir
.filePath(filename
.filePath()));
589 ////////////////////////////////////////////////////////////
591 ////////////////////////////////////////////////////////////
593 unsigned int FileAnalyzer::filesAccepted(void)
595 return m_filesAccepted
;
598 unsigned int FileAnalyzer::filesRejected(void)
600 return m_filesRejected
- m_filesDenied
;
603 unsigned int FileAnalyzer::filesDenied(void)
605 return m_filesDenied
;
608 ////////////////////////////////////////////////////////////
610 ////////////////////////////////////////////////////////////