Improve ASX/WPL parser. Should be more XML-conform now.
[LameXP.git] / src / Thread_FileAnalyzer.cpp
bloba3c2756748c7fabd246810c492016087e41cbda5
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"
28 #include <QDir>
29 #include <QFileInfo>
30 #include <QProcess>
31 #include <QDate>
32 #include <QTime>
33 #include <QDebug>
34 #include <QMessageBox>
36 #include <math.h>
38 //Un-escape XML characters
39 #define XML_DECODE replace("&amp;", "&").replace("&apos;", "'").replace("&nbsp;", " ").replace("&quot;", "\"").replace("&lt;", "<").replace("&gt;", ">")
41 ////////////////////////////////////////////////////////////
42 // Constructor
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"))
51 m_bSuccess = false;
53 if(m_mediaInfoBin_x86.isEmpty() || m_mediaInfoBin_x64.isEmpty())
55 qFatal("Invalid path to MediaInfo binary. Tool not initialized properly.");
58 m_filesAccepted = 0;
59 m_filesRejected = 0;
60 m_filesDenied = 0;
63 ////////////////////////////////////////////////////////////
64 // Thread Main
65 ////////////////////////////////////////////////////////////
67 void FileAnalyzer::run()
69 m_bSuccess = false;
71 m_filesAccepted = 0;
72 m_filesRejected = 0;
73 m_filesDenied = 0;
75 m_inputFiles.sort();
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))
87 m_filesRejected++;
88 qDebug("Skipped: %s", file.filePath().toUtf8().constData());
90 continue;
92 m_filesAccepted++;
93 emit fileAnalyzed(file);
96 qDebug("All files added.\n");
97 m_bSuccess = true;
100 ////////////////////////////////////////////////////////////
101 // Privtae Functions
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!");
116 m_filesDenied++;
117 return audioFile;
119 else
121 readTest.close();
124 QProcess process;
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());
133 process.kill();
134 process.waitForFinished(-1);
135 return audioFile;
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!");
145 process.kill();
146 process.waitForFinished(-1);
147 return audioFile;
151 QByteArray data;
153 while(process.canReadLine())
155 QString line = QString::fromUtf8(process.readLine().constData()).simplified();
156 if(!line.isEmpty())
158 int index = line.indexOf(':');
159 if(index > 0)
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);
168 else
170 updateSection(line);
176 if(audioFile.fileName().isEmpty())
178 QString baseName = QFileInfo(filePath).fileName();
179 int index = baseName.lastIndexOf(".");
181 if(index >= 0)
183 baseName = baseName.left(index);
186 baseName = baseName.replace("_", " ").simplified();
187 index = baseName.lastIndexOf(" - ");
189 if(index >= 0)
191 baseName = baseName.mid(index + 3).trimmed();
194 audioFile.setFileName(baseName);
197 return audioFile;
200 void FileAnalyzer::updateSection(const QString &section)
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;
215 else
217 qWarning("Unknown section: %s", section.toUtf8().constData());
221 void FileAnalyzer::updateInfo(AudioFileModel &audioFile, const QString &key, const QString &value)
223 switch(m_currentSection)
225 case sectionGeneral:
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);
266 break;
268 case sectionAudio:
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));
301 break;
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");
310 if(date.isValid())
312 return date.year();
314 else
316 return 0;
319 else
321 bool ok = false;
322 int year = str.toInt(&ok);
323 if(ok && year > 0)
325 return year;
327 else
329 return 0;
334 unsigned int FileAnalyzer::parseDuration(const QString &str)
336 QTime time;
338 time = QTime::fromString(str, "z'ms'");
339 if(time.isValid())
341 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
344 time = QTime::fromString(str, "s's 'z'ms'");
345 if(time.isValid())
347 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
350 time = QTime::fromString(str, "m'mn 's's'");
351 if(time.isValid())
353 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
356 time = QTime::fromString(str, "h'h 'm'mn'");
357 if(time.isValid())
359 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
362 return 0;
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());
374 //Sanity check
375 if(file.size() < 3 || file.size() > 512000)
377 return false;
380 //Detect playlist type
381 playlist_t playlistType = isPlaylist(file.canonicalFilePath());
383 //Exit if not a playlist
384 if(playlistType == noPlaylist)
386 return false;
389 QFile data(playlistFile);
391 //Open file for reading
392 if(!data.open(QIODevice::ReadOnly))
394 return false;
397 //Parse playlist depending on type
398 switch(playlistType)
400 case m3uPlaylist:
401 return parsePlaylist_m3u(data, fileList, baseDir, rootDir);
402 break;
403 case plsPlaylist:
404 return parsePlaylist_pls(data, fileList, baseDir, rootDir);
405 break;
406 case wplPlaylist:
407 return parsePlaylist_wpl(data, fileList, baseDir, rootDir);
408 break;
409 default:
410 return false;
411 break;
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();
451 return true;
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)
461 bool flag = false;
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();
477 flag = true;
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();
493 flag = true;
498 line = data.readLine();
501 return true;
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();
513 buffer.clear();
515 int index = 0;
517 while((index = skipData.indexIn(line)) >= 0)
519 line.remove(index, skipData.matchedLength());
522 int offset = 0;
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();
539 return true;
542 FileAnalyzer::playlist_t FileAnalyzer::isPlaylist(const QString &fileName)
544 QFileInfo file (fileName);
546 if(file.suffix().compare("m3u", Qt::CaseInsensitive) == 0)
548 return m3uPlaylist;
550 else if(file.suffix().compare("m3u8", Qt::CaseInsensitive) == 0)
552 return m3uPlaylist;
554 else if(file.suffix().compare("pls", Qt::CaseInsensitive) == 0)
556 return plsPlaylist;
558 else if(file.suffix().compare("asx", Qt::CaseInsensitive) == 0)
560 return wplPlaylist;
562 else if(file.suffix().compare("wpl", Qt::CaseInsensitive) == 0)
564 return wplPlaylist;
566 else
568 return noPlaylist;
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 ////////////////////////////////////////////////////////////
590 // Public Functions
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 ////////////////////////////////////////////////////////////
609 // EVENTS
610 ////////////////////////////////////////////////////////////
612 /*NONE*/