OSDN Git Service

Added workaround for MediaInfo's line break bug (regression in latest MediaInfo).
[lamexp/LameXP.git] / src / Thread_FileAnalyzer.cpp
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.
14 //
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.
18 //
19 // http://www.gnu.org/licenses/gpl-2.0.txt
20 ///////////////////////////////////////////////////////////////////////////////
21
22 #include "Thread_FileAnalyzer.h"
23
24 #include "Global.h"
25 #include "LockedFile.h"
26 #include "Model_AudioFile.h"
27 #include "PlaylistImporter.h"
28
29 #include <QDir>
30 #include <QFileInfo>
31 #include <QProcess>
32 #include <QDate>
33 #include <QTime>
34 #include <QDebug>
35
36 #include <math.h>
37
38 ////////////////////////////////////////////////////////////
39 // Constructor
40 ////////////////////////////////////////////////////////////
41
42 FileAnalyzer::FileAnalyzer(const QStringList &inputFiles)
43 :
44         m_inputFiles(inputFiles),
45         m_mediaInfoBin(lamexp_lookup_tool("mediainfo.exe"))
46 {
47         m_bSuccess = false;
48         
49         if(m_mediaInfoBin.isEmpty())
50         {
51                 qFatal("Invalid path to MediaInfo binary. Tool not initialized properly.");
52         }
53
54         m_filesAccepted = 0;
55         m_filesRejected = 0;
56         m_filesDenied = 0;
57         m_filesDummyCDDA = 0;
58 }
59
60 ////////////////////////////////////////////////////////////
61 // Thread Main
62 ////////////////////////////////////////////////////////////
63
64 void FileAnalyzer::run()
65 {
66         m_bSuccess = false;
67
68         m_filesAccepted = 0;
69         m_filesRejected = 0;
70         m_filesDenied = 0;
71         m_filesDummyCDDA = 0;
72         m_inputFiles.sort();
73
74         GetAsyncKeyState(VK_ESCAPE);
75
76         while(!m_inputFiles.isEmpty())
77         {
78                 if(GetAsyncKeyState(VK_ESCAPE) & 0x0001)
79                 {
80                         MessageBeep(MB_ICONERROR);
81                         qWarning("Operation cancelled by user!");
82                         break;
83                 }
84                 
85                 QString currentFile = QDir::fromNativeSeparators(m_inputFiles.takeFirst());
86                 qDebug64("Analyzing: %1", currentFile);
87                 emit fileSelected(QFileInfo(currentFile).fileName());
88                 AudioFileModel file = analyzeFile(currentFile);
89                 
90                 if(file.fileName().isEmpty() || file.formatContainerType().isEmpty() || file.formatAudioType().isEmpty())
91                 {
92                         if(!PlaylistImporter::importPlaylist(m_inputFiles, currentFile))
93                         {
94                                 m_filesRejected++;
95                                 qDebug64("Skipped: %1", file.filePath());
96                         }
97                         continue;
98                 }
99                 
100                 m_filesAccepted++;
101                 emit fileAnalyzed(file);
102         }
103
104         qDebug("All files added.\n");
105         m_bSuccess = true;
106 }
107
108 ////////////////////////////////////////////////////////////
109 // Privtae Functions
110 ////////////////////////////////////////////////////////////
111
112 const AudioFileModel FileAnalyzer::analyzeFile(const QString &filePath)
113 {
114         AudioFileModel audioFile(filePath);
115         m_currentSection = sectionOther;
116         m_currentCover = coverNone;
117         m_lineBreakBugWorkaround = false;
118
119         QFile readTest(filePath);
120         if(!readTest.open(QIODevice::ReadOnly))
121         {
122                 qWarning("Cannot access file for reading, skipping!");
123                 m_filesDenied++;
124                 return audioFile;
125         }
126         
127         if(checkFile_CDDA(readTest))
128         {
129                 qWarning("Dummy CDDA file detected, skipping!");
130                 m_filesDummyCDDA ++;
131                 return audioFile;
132         }
133         
134         readTest.close();
135
136         QProcess process;
137         process.setProcessChannelMode(QProcess::MergedChannels);
138         process.setReadChannel(QProcess::StandardOutput);
139         process.start(m_mediaInfoBin, QStringList() << QDir::toNativeSeparators(filePath));
140                 
141         if(!process.waitForStarted())
142         {
143                 qWarning("MediaInfo process failed to create!");
144                 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
145                 process.kill();
146                 process.waitForFinished(-1);
147                 return audioFile;
148         }
149
150         while(process.state() != QProcess::NotRunning)
151         {
152                 if(!process.waitForReadyRead())
153                 {
154                         if(process.state() == QProcess::Running)
155                         {
156                                 qWarning("MediaInfo time out. Killing process and skipping file!");
157                                 process.kill();
158                                 process.waitForFinished(-1);
159                                 return audioFile;
160                         }
161                 }
162
163                 QByteArray data;
164
165                 while(process.canReadLine())
166                 {
167                         QString line = QString::fromUtf8(process.readLine().constData()).simplified();
168                         if(!line.isEmpty())
169                         {
170                                 int index = line.indexOf(':');
171                                 if(index > 0)
172                                 {
173                                         m_lineBreakBugWorkaround = false;
174                                         QString key = line.left(index-1).trimmed();
175                                         QString val = line.mid(index+1).trimmed();
176                                         if(!key.isEmpty() && !val.isEmpty())
177                                         {
178                                                 updateInfo(audioFile, key, val);
179                                         }
180                                 }
181                                 else
182                                 {
183                                         updateSection(line);
184                                 }
185                         }
186                 }
187         }
188
189         if(audioFile.fileName().isEmpty())
190         {
191                 QString baseName = QFileInfo(filePath).fileName();
192                 int index = baseName.lastIndexOf(".");
193
194                 if(index >= 0)
195                 {
196                         baseName = baseName.left(index);
197                 }
198
199                 baseName = baseName.replace("_", " ").simplified();
200                 index = baseName.lastIndexOf(" - ");
201
202                 if(index >= 0)
203                 {
204                         baseName = baseName.mid(index + 3).trimmed();
205                 }
206
207                 audioFile.setFileName(baseName);
208         }
209         
210         if(m_currentCover != coverNone)
211         {
212                 retrieveCover(audioFile, filePath);
213         }
214
215         return audioFile;
216 }
217
218 void FileAnalyzer::updateSection(const QString &section)
219 {
220         if(section.startsWith("General", Qt::CaseInsensitive))
221         {
222                 m_currentSection = sectionGeneral;
223         }
224         else if(!section.compare("Audio", Qt::CaseInsensitive) || section.startsWith("Audio #1", Qt::CaseInsensitive))
225         {
226                 m_currentSection = sectionAudio;
227         }
228         else if(section.startsWith("Audio", Qt::CaseInsensitive) || section.startsWith("Video", Qt::CaseInsensitive) || section.startsWith("Text", Qt::CaseInsensitive) ||
229                 section.startsWith("Menu", Qt::CaseInsensitive) || section.startsWith("Image", Qt::CaseInsensitive) || section.startsWith("Chapters", Qt::CaseInsensitive))
230         {
231                 m_currentSection = sectionOther;
232         }
233         else
234         {
235                 if(!m_lineBreakBugWorkaround)
236                 {
237                         m_currentSection = sectionOther;
238                         qWarning("Unknown section: %s", section.toUtf8().constData());
239                 }
240         }
241 }
242
243 void FileAnalyzer::updateInfo(AudioFileModel &audioFile, const QString &key, const QString &value)
244 {
245         switch(m_currentSection)
246         {
247         case sectionGeneral:
248                 if(!key.compare("Title", Qt::CaseInsensitive) || !key.compare("Track", Qt::CaseInsensitive) || !key.compare("Track Name", Qt::CaseInsensitive))
249                 {
250                         if(audioFile.fileName().isEmpty()) audioFile.setFileName(value);
251                 }
252                 else if(!key.compare("Duration", Qt::CaseInsensitive))
253                 {
254                         if(!audioFile.fileDuration()) audioFile.setFileDuration(parseDuration(value));
255                 }
256                 else if(!key.compare("Artist", Qt::CaseInsensitive) || !key.compare("Performer", Qt::CaseInsensitive))
257                 {
258                         if(audioFile.fileArtist().isEmpty()) audioFile.setFileArtist(value);
259                 }
260                 else if(!key.compare("Album", Qt::CaseInsensitive))
261                 {
262                         if(audioFile.fileAlbum().isEmpty()) audioFile.setFileAlbum(value);
263                 }
264                 else if(!key.compare("Genre", Qt::CaseInsensitive))
265                 {
266                         if(audioFile.fileGenre().isEmpty()) audioFile.setFileGenre(value);
267                 }
268                 else if(!key.compare("Year", Qt::CaseInsensitive) || !key.compare("Recorded Date", Qt::CaseInsensitive) || !key.compare("Encoded Date", Qt::CaseInsensitive))
269                 {
270                         if(!audioFile.fileYear()) audioFile.setFileYear(parseYear(value));
271                 }
272                 else if(!key.compare("Comment", Qt::CaseInsensitive))
273                 {
274                         if(audioFile.fileComment().isEmpty()) audioFile.setFileComment(value);
275                 }
276                 else if(!key.compare("Track Name/Position", Qt::CaseInsensitive))
277                 {
278                         if(!audioFile.filePosition()) audioFile.setFilePosition(value.toInt());
279                 }
280                 else if(!key.compare("Format", Qt::CaseInsensitive))
281                 {
282                         if(audioFile.formatContainerType().isEmpty()) audioFile.setFormatContainerType(value);
283                 }
284                 else if(!key.compare("Format Profile", Qt::CaseInsensitive))
285                 {
286                         if(audioFile.formatContainerProfile().isEmpty()) audioFile.setFormatContainerProfile(value);
287                 }
288                 else if(!key.compare("Cover", Qt::CaseInsensitive) || !key.compare("Cover type", Qt::CaseInsensitive))
289                 {
290                         if(m_currentCover == coverNone) m_currentCover = coverJpeg;
291                 }
292                 else if(!key.compare("Cover MIME", Qt::CaseInsensitive))
293                 {
294                         QString temp = value.split(" ", QString::SkipEmptyParts, Qt::CaseInsensitive).first();
295                         if(!temp.compare("image/jpeg", Qt::CaseInsensitive))
296                         {
297                                 m_currentCover = coverJpeg;
298                         }
299                         else if(!temp.compare("image/png", Qt::CaseInsensitive))
300                         {
301                                 m_currentCover = coverPng;
302                         }
303                         else if(!temp.compare("image/gif", Qt::CaseInsensitive))
304                         {
305                                 m_currentCover = coverGif;
306                         }
307                 }
308                 else if(!key.compare("Complete Name", Qt::CaseInsensitive))
309                 {
310                         m_lineBreakBugWorkaround = true;
311                 }
312                 break;
313
314         case sectionAudio:
315                 if(!key.compare("Year", Qt::CaseInsensitive) || !key.compare("Recorded Date", Qt::CaseInsensitive) || !key.compare("Encoded Date", Qt::CaseInsensitive))
316                 {
317                         if(!audioFile.fileYear()) audioFile.setFileYear(parseYear(value));
318                 }
319                 else if(!key.compare("Format", Qt::CaseInsensitive))
320                 {
321                         if(audioFile.formatAudioType().isEmpty()) audioFile.setFormatAudioType(value);
322                 }
323                 else if(!key.compare("Format Profile", Qt::CaseInsensitive))
324                 {
325                         if(audioFile.formatAudioProfile().isEmpty()) audioFile.setFormatAudioProfile(value);
326                 }
327                 else if(!key.compare("Format Version", Qt::CaseInsensitive))
328                 {
329                         if(audioFile.formatAudioVersion().isEmpty()) audioFile.setFormatAudioVersion(value);
330                 }
331                 else if(!key.compare("Channel(s)", Qt::CaseInsensitive))
332                 {
333                         if(!audioFile.formatAudioChannels()) audioFile.setFormatAudioChannels(value.split(" ", QString::SkipEmptyParts).first().toInt());
334                 }
335                 else if(!key.compare("Sampling rate", Qt::CaseInsensitive))
336                 {
337                         if(!audioFile.formatAudioSamplerate()) audioFile.setFormatAudioSamplerate(ceil(value.split(" ", QString::SkipEmptyParts).first().toFloat() * 1000.0f));
338                 }
339                 else if(!key.compare("Bit depth", Qt::CaseInsensitive))
340                 {
341                         if(!audioFile.formatAudioBitdepth()) audioFile.setFormatAudioBitdepth(value.split(" ", QString::SkipEmptyParts).first().toInt());
342                 }
343                 else if(!key.compare("Duration", Qt::CaseInsensitive))
344                 {
345                         if(!audioFile.fileDuration()) audioFile.setFileDuration(parseDuration(value));
346                 }
347                 break;
348         }
349 }
350
351 unsigned int FileAnalyzer::parseYear(const QString &str)
352 {
353         if(str.startsWith("UTC", Qt::CaseInsensitive))
354         {
355                 QDate date = QDate::fromString(str.mid(3).trimmed().left(10), "yyyy-MM-dd");
356                 if(date.isValid())
357                 {
358                         return date.year();
359                 }
360                 else
361                 {
362                         return 0;
363                 }
364         }
365         else
366         {
367                 bool ok = false;
368                 int year = str.toInt(&ok);
369                 if(ok && year > 0)
370                 {
371                         return year;
372                 }
373                 else
374                 {
375                         return 0;
376                 }
377         }
378 }
379
380 unsigned int FileAnalyzer::parseDuration(const QString &str)
381 {
382         QTime time;
383
384         time = QTime::fromString(str, "z'ms'");
385         if(time.isValid())
386         {
387                 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
388         }
389
390         time = QTime::fromString(str, "s's 'z'ms'");
391         if(time.isValid())
392         {
393                 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
394         }
395
396         time = QTime::fromString(str, "m'mn 's's'");
397         if(time.isValid())
398         {
399                 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
400         }
401
402         time = QTime::fromString(str, "h'h 'm'mn'");
403         if(time.isValid())
404         {
405                 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
406         }
407
408         return 0;
409 }
410
411 bool FileAnalyzer::checkFile_CDDA(QFile &file)
412 {
413         file.reset();
414         QByteArray data = file.read(128);
415         
416         int i = data.indexOf("RIFF");
417         int j = data.indexOf("CDDA");
418         int k = data.indexOf("fmt ");
419
420         return ((i >= 0) && (j >= 0) && (k >= 0) && (k > j) && (j > i));
421 }
422
423 void FileAnalyzer::retrieveCover(AudioFileModel &audioFile, const QString &filePath)
424 {
425         qDebug64("Retrieving cover from: %1", filePath);
426         QString extension;
427
428         switch(m_currentCover)
429         {
430         case coverPng:
431                 extension = QString::fromLatin1("png");
432                 break;
433         case coverGif:
434                 extension = QString::fromLatin1("gif");
435                 break;
436         default:
437                 extension = QString::fromLatin1("jpg");
438                 break;
439         }
440         
441         QProcess process;
442         process.setProcessChannelMode(QProcess::MergedChannels);
443         process.setReadChannel(QProcess::StandardOutput);
444         process.start(m_mediaInfoBin, QStringList() << "-f" << QDir::toNativeSeparators(filePath));
445         
446         if(!process.waitForStarted())
447         {
448                 qWarning("MediaInfo process failed to create!");
449                 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
450                 process.kill();
451                 process.waitForFinished(-1);
452                 return;
453         }
454
455         while(process.state() != QProcess::NotRunning)
456         {
457                 if(!process.waitForReadyRead())
458                 {
459                         if(process.state() == QProcess::Running)
460                         {
461                                 qWarning("MediaInfo time out. Killing process and skipping file!");
462                                 process.kill();
463                                 process.waitForFinished(-1);
464                                 return;
465                         }
466                 }
467
468                 while(process.canReadLine())
469                 {
470                         QString line = QString::fromUtf8(process.readLine().constData()).simplified();
471                         if(!line.isEmpty())
472                         {
473                                 int index = line.indexOf(':');
474                                 if(index > 0)
475                                 {
476                                         QString key = line.left(index-1).trimmed();
477                                         QString val = line.mid(index+1).trimmed();
478                                         if(!key.isEmpty() && !val.isEmpty())
479                                         {
480                                                 if(!key.compare("Cover_Data", Qt::CaseInsensitive))
481                                                 {
482                                                         if(val.indexOf(" ") > 0)
483                                                         {
484                                                                 val = val.split(" ", QString::SkipEmptyParts, Qt::CaseInsensitive).first();
485                                                         }
486                                                         QByteArray coverData = QByteArray::fromBase64(val.toLatin1());
487                                                         QFile coverFile(QString("%1/%2.%3").arg(lamexp_temp_folder2(), lamexp_rand_str(), extension));
488                                                         if(coverFile.open(QIODevice::WriteOnly))
489                                                         {
490                                                                 coverFile.write(coverData);
491                                                                 coverFile.close();
492                                                                 audioFile.setFileCover(coverFile.fileName(), true);
493                                                         }
494                                                         break;
495                                                 }
496                                         }
497                                 }
498                         }
499                 }
500         }
501 }
502
503 ////////////////////////////////////////////////////////////
504 // Public Functions
505 ////////////////////////////////////////////////////////////
506
507 unsigned int FileAnalyzer::filesAccepted(void)
508 {
509         return m_filesAccepted;
510 }
511
512 unsigned int FileAnalyzer::filesRejected(void)
513 {
514         return max(m_filesRejected - (m_filesDenied + m_filesDummyCDDA), 0);
515 }
516
517 unsigned int FileAnalyzer::filesDenied(void)
518 {
519         return m_filesDenied;
520 }
521
522 unsigned int FileAnalyzer::filesDummyCDDA(void)
523 {
524         return m_filesDummyCDDA;
525 }
526
527 ////////////////////////////////////////////////////////////
528 // EVENTS
529 ////////////////////////////////////////////////////////////
530
531 /*NONE*/