OSDN Git Service

Set creation/modified time of the encoded file the same value as the original file...
[lamexp/LameXP.git] / src / Thread_FileAnalyzer_Task.cpp
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2015 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, but always including the *additional*
9 // restrictions defined in the "License.txt" file.
10 //
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 // GNU General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License along
17 // with this program; if not, write to the Free Software Foundation, Inc.,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 //
20 // http://www.gnu.org/licenses/gpl-2.0.txt
21 ///////////////////////////////////////////////////////////////////////////////
22
23 #include "Thread_FileAnalyzer_Task.h"
24
25 //Internal
26 #include "Global.h"
27 #include "LockedFile.h"
28 #include "Model_AudioFile.h"
29 #include "MimeTypes.h"
30
31 //MUtils
32 #include <MUtils/Global.h>
33 #include <MUtils/OSSupport.h>
34 #include <MUtils/Exception.h>
35
36 //Qt
37 #include <QDir>
38 #include <QFileInfo>
39 #include <QProcess>
40 #include <QDate>
41 #include <QTime>
42 #include <QDebug>
43 #include <QImage>
44 #include <QReadLocker>
45 #include <QWriteLocker>
46 #include <QThread>
47
48
49 //CRT
50 #include <math.h>
51 #include <time.h>
52 #include <assert.h>
53
54 #define IS_KEY(KEY) (key.compare(KEY, Qt::CaseInsensitive) == 0)
55 #define IS_SEC(SEC) (key.startsWith((SEC "_"), Qt::CaseInsensitive))
56 #define FIRST_TOK(STR) (STR.split(" ", QString::SkipEmptyParts).first())
57
58 ////////////////////////////////////////////////////////////
59 // Constructor
60 ////////////////////////////////////////////////////////////
61
62 AnalyzeTask::AnalyzeTask(const int taskId, const QString &inputFile, const QString &templateFile, volatile bool *abortFlag)
63 :
64         m_taskId(taskId),
65         m_inputFile(inputFile),
66         m_templateFile(templateFile),
67         m_mediaInfoBin(lamexp_tools_lookup("mediainfo.exe")),
68         m_avs2wavBin(lamexp_tools_lookup("avs2wav.exe")),
69         m_abortFlag(abortFlag)
70 {
71         if(m_mediaInfoBin.isEmpty() || m_avs2wavBin.isEmpty())
72         {
73                 qFatal("Invalid path to MediaInfo binary. Tool not initialized properly.");
74         }
75 }
76
77 AnalyzeTask::~AnalyzeTask(void)
78 {
79         emit taskCompleted(m_taskId);
80 }
81
82 ////////////////////////////////////////////////////////////
83 // Thread Main
84 ////////////////////////////////////////////////////////////
85
86 void AnalyzeTask::run()
87 {
88         try
89         {
90                 run_ex();
91         }
92         catch(const std::exception &error)
93         {
94                 MUTILS_PRINT_ERROR("\nGURU MEDITATION !!!\n\nException error:\n%s\n", error.what());
95                 MUtils::OS::fatal_exit(L"Unhandeled C++ exception error, application will exit!");
96         }
97         catch(...)
98         {
99                 MUTILS_PRINT_ERROR("\nGURU MEDITATION !!!\n\nUnknown exception error!\n");
100                 MUtils::OS::fatal_exit(L"Unhandeled C++ exception error, application will exit!");
101         }
102 }
103
104 void AnalyzeTask::run_ex(void)
105 {
106         int fileType = fileTypeNormal;
107         QString currentFile = QDir::fromNativeSeparators(m_inputFile);
108         qDebug("Analyzing: %s", MUTILS_UTF8(currentFile));
109         
110         AudioFileModel file = analyzeFile(currentFile, &fileType);
111
112         if(*m_abortFlag)
113         {
114                 qWarning("Operation cancelled by user!");
115                 return;
116         }
117
118         switch(fileType)
119         {
120         case fileTypeDenied:
121                 qWarning("Cannot access file for reading, skipping!");
122                 break;
123         case fileTypeCDDA:
124                 qWarning("Dummy CDDA file detected, skipping!");
125                 break;
126         default:
127                 if(file.metaInfo().title().isEmpty() || file.techInfo().containerType().isEmpty() || file.techInfo().audioType().isEmpty())
128                 {
129                         fileType = fileTypeUnknown;
130                         if(!QFileInfo(currentFile).suffix().compare("cue", Qt::CaseInsensitive))
131                         {
132                                 qWarning("Cue Sheet file detected, skipping!");
133                                 fileType = fileTypeCueSheet;
134                         }
135                         else if(!QFileInfo(currentFile).suffix().compare("avs", Qt::CaseInsensitive))
136                         {
137                                 qDebug("Found a potential Avisynth script, investigating...");
138                                 if(analyzeAvisynthFile(currentFile, file))
139                                 {
140                                         fileType = fileTypeNormal;
141                                 }
142                                 else
143                                 {
144                                         qDebug("Rejected Avisynth file: %s", MUTILS_UTF8(file.filePath()));
145                                 }
146                         }
147                         else
148                         {
149                                 qDebug("Rejected file of unknown type: %s", MUTILS_UTF8(file.filePath()));
150                         }
151                 }
152                 break;
153         }
154
155         //Emit the file now!
156         emit fileAnalyzed(m_taskId, fileType, file);
157 }
158
159 ////////////////////////////////////////////////////////////
160 // Privtae Functions
161 ////////////////////////////////////////////////////////////
162
163 const AudioFileModel AnalyzeTask::analyzeFile(const QString &filePath, int *type)
164 {
165         *type = fileTypeNormal;
166         AudioFileModel audioFile(filePath);
167
168         QFile readTest(filePath);
169         if(!readTest.open(QIODevice::ReadOnly))
170         {
171                 *type = fileTypeDenied;
172                 return audioFile;
173         }
174         if(checkFile_CDDA(readTest))
175         {
176                 *type = fileTypeCDDA;
177                 return audioFile;
178         }
179         readTest.close();
180
181         bool skipNext = false;
182         QPair<quint32, quint32> id_val(UINT_MAX, UINT_MAX);
183         quint32 coverType = UINT_MAX;
184         QByteArray coverData;
185
186         QStringList params;
187         params << QString("--Inform=file://%1").arg(QDir::toNativeSeparators(m_templateFile));
188         params << QDir::toNativeSeparators(filePath);
189         
190         QProcess process;
191         MUtils::init_process(process, QFileInfo(m_mediaInfoBin).absolutePath());
192
193         process.start(m_mediaInfoBin, params);
194                 
195         if(!process.waitForStarted())
196         {
197                 qWarning("MediaInfo process failed to create!");
198                 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
199                 process.kill();
200                 process.waitForFinished(-1);
201                 return audioFile;
202         }
203
204         while(process.state() != QProcess::NotRunning)
205         {
206                 if(*m_abortFlag)
207                 {
208                         process.kill();
209                         qWarning("Process was aborted on user request!");
210                         break;
211                 }
212                 
213                 if(!process.waitForReadyRead())
214                 {
215                         if(process.state() == QProcess::Running)
216                         {
217                                 qWarning("MediaInfo time out. Killing process and skipping file!");
218                                 process.kill();
219                                 process.waitForFinished(-1);
220                                 return audioFile;
221                         }
222                 }
223
224                 QByteArray data;
225
226                 while(process.canReadLine())
227                 {
228                         QString line = QString::fromUtf8(process.readLine().constData()).simplified();
229                         if(!line.isEmpty())
230                         {
231                                 //qDebug("Line:%s", MUTILS_UTF8(line));
232                                 
233                                 int index = line.indexOf('=');
234                                 if(index > 0)
235                                 {
236                                         QString key = line.left(index).trimmed();
237                                         QString val = line.mid(index+1).trimmed();
238                                         if(!key.isEmpty())
239                                         {
240                                                 updateInfo(audioFile, skipNext, id_val, coverType, coverData, key, val);
241                                         }
242                                 }
243                         }
244                 }
245         }
246
247         if(audioFile.metaInfo().title().isEmpty())
248         {
249                 QString baseName = QFileInfo(filePath).fileName();
250                 int index = baseName.lastIndexOf(".");
251
252                 if(index >= 0)
253                 {
254                         baseName = baseName.left(index);
255                 }
256
257                 baseName = baseName.replace("_", " ").simplified();
258                 index = baseName.lastIndexOf(" - ");
259
260                 if(index >= 0)
261                 {
262                         baseName = baseName.mid(index + 3).trimmed();
263                 }
264
265                 audioFile.metaInfo().setTitle(baseName);
266         }
267         
268         process.waitForFinished();
269         if(process.state() != QProcess::NotRunning)
270         {
271                 process.kill();
272                 process.waitForFinished(-1);
273         }
274
275         if((coverType != UINT_MAX) && (!coverData.isEmpty()))
276         {
277                 retrieveCover(audioFile, coverType, coverData);
278         }
279
280         if((audioFile.techInfo().audioType().compare("PCM", Qt::CaseInsensitive) == 0) && (audioFile.techInfo().audioProfile().compare("Float", Qt::CaseInsensitive) == 0))
281         {
282                 if(audioFile.techInfo().audioBitdepth() == 32) audioFile.techInfo().setAudioBitdepth(AudioFileModel::BITDEPTH_IEEE_FLOAT32);
283         }
284
285         return audioFile;
286 }
287
288 void AnalyzeTask::updateInfo(AudioFileModel &audioFile, bool &skipNext, QPair<quint32, quint32> &id_val, quint32 &coverType, QByteArray &coverData, const QString &key, const QString &value)
289 {
290         //qWarning("'%s' -> '%s'", MUTILS_UTF8(key), MUTILS_UTF8(value));
291         
292         /*New Stream*/
293         if(IS_KEY("Gen_ID") || IS_KEY("Aud_ID"))
294         {
295                 if(value.isEmpty())
296                 {
297                         skipNext = false;
298                 }
299                 else
300                 {
301                         //We ignore all ID's, except for the lowest one!
302                         bool ok = false;
303                         unsigned int id = value.toUInt(&ok);
304                         if(ok)
305                         {
306                                 if(IS_KEY("Gen_ID")) { id_val.first  = qMin(id_val.first,  id); skipNext = (id > id_val.first);  }
307                                 if(IS_KEY("Aud_ID")) { id_val.second = qMin(id_val.second, id); skipNext = (id > id_val.second); }
308                         }
309                         else
310                         {
311                                 skipNext = true;
312                         }
313                 }
314                 if(skipNext)
315                 {
316                         qWarning("Skipping info for non-primary stream!");
317                 }
318                 return;
319         }
320
321         /*Skip or empty?*/
322         if((skipNext) || value.isEmpty())
323         {
324                 return;
325         }
326
327         /*Playlist file?*/
328         if(IS_KEY("Aud_Source"))
329         {
330                 skipNext = true;
331                 audioFile.techInfo().setContainerType(QString());
332                 audioFile.techInfo().setAudioType(QString());
333                 qWarning("Skipping info for playlist file!");
334                 return;
335         }
336
337         /*General Section*/
338         if(IS_SEC("Gen"))
339         {
340                 if(IS_KEY("Gen_Format"))
341                 {
342                         audioFile.techInfo().setContainerType(value);
343                 }
344                 else if(IS_KEY("Gen_Format_Profile"))
345                 {
346                         audioFile.techInfo().setContainerProfile(value);
347                 }
348                 else if(IS_KEY("Gen_Title") || IS_KEY("Gen_Track"))
349                 {
350                         audioFile.metaInfo().setTitle(value);
351                 }
352                 else if(IS_KEY("Gen_Duration"))
353                 {
354                         unsigned int tmp = parseDuration(value);
355                         if(tmp > 0) audioFile.techInfo().setDuration(tmp);
356                 }
357                 else if(IS_KEY("Gen_Artist") || IS_KEY("Gen_Performer"))
358                 {
359                         audioFile.metaInfo().setArtist(value);
360                 }
361                 else if(IS_KEY("Gen_Album"))
362                 {
363                         audioFile.metaInfo().setAlbum(value);
364                 }
365                 else if(IS_KEY("Gen_Genre"))
366                 {
367                         audioFile.metaInfo().setGenre(value);
368                 }
369                 else if(IS_KEY("Gen_Released_Date") || IS_KEY("Gen_Recorded_Date"))
370                 {
371                         unsigned int tmp = parseYear(value);
372                         if(tmp > 0) audioFile.metaInfo().setYear(tmp);
373                 }
374                 else if(IS_KEY("Gen_Comment"))
375                 {
376                         audioFile.metaInfo().setComment(value);
377                 }
378                 else if(IS_KEY("Gen_Track/Position"))
379                 {
380                         bool ok = false;
381                         unsigned int tmp = value.toUInt(&ok);
382                         if(ok) audioFile.metaInfo().setPosition(tmp);
383                 }
384                 else if(IS_KEY("Gen_Cover") || IS_KEY("Gen_Cover_Type"))
385                 {
386                         if(coverType == UINT_MAX)
387                         {
388                                 coverType = 0;
389                         }
390                 }
391                 else if(IS_KEY("Gen_Cover_Mime"))
392                 {
393                         QString temp = FIRST_TOK(value);
394                         for (quint32 i = 0; MIME_TYPES[i].type; i++)
395                         {
396                                 if (temp.compare(QString::fromLatin1(MIME_TYPES[i].type), Qt::CaseInsensitive) == 0)
397                                 {
398                                         coverType = i;
399                                         break;
400                                 }
401                         }
402                 }
403                 else if(IS_KEY("Gen_Cover_Data"))
404                 {
405                         if(!coverData.isEmpty()) coverData.clear();
406                         coverData.append(QByteArray::fromBase64(FIRST_TOK(value).toLatin1()));
407                 }
408                 else
409                 {
410                         qWarning("Unknown key '%s' with value '%s' found!", MUTILS_UTF8(key), MUTILS_UTF8(value));
411                 }
412                 return;
413         }
414
415         /*Audio Section*/
416         if(IS_SEC("Aud"))
417         {
418
419                 if(IS_KEY("Aud_Format"))
420                 {
421                         audioFile.techInfo().setAudioType(value);
422                 }
423                 else if(IS_KEY("Aud_Format_Profile"))
424                 {
425                         audioFile.techInfo().setAudioProfile(value);
426                 }
427                 else if(IS_KEY("Aud_Format_Version"))
428                 {
429                         audioFile.techInfo().setAudioVersion(value);
430                 }
431                 else if(IS_KEY("Aud_Channel(s)"))
432                 {
433                         bool ok = false;
434                         unsigned int tmp = value.toUInt(&ok);
435                         if(ok) audioFile.techInfo().setAudioChannels(tmp);
436                 }
437                 else if(IS_KEY("Aud_SamplingRate"))
438                 {
439                         bool ok = false;
440                         unsigned int tmp = value.toUInt(&ok);
441                         if(ok) audioFile.techInfo().setAudioSamplerate(tmp);
442                 }
443                 else if(IS_KEY("Aud_BitDepth"))
444                 {
445                         bool ok = false;
446                         unsigned int tmp = value.toUInt(&ok);
447                         if(ok) audioFile.techInfo().setAudioBitdepth(tmp);
448                 }
449                 else if(IS_KEY("Aud_Duration"))
450                 {
451                         unsigned int tmp = parseDuration(value);
452                         if(tmp > 0) audioFile.techInfo().setDuration(tmp);
453                 }
454                 else if(IS_KEY("Aud_BitRate"))
455                 {
456                         bool ok = false;
457                         unsigned int tmp = value.toUInt(&ok);
458                         if(ok) audioFile.techInfo().setAudioBitrate(tmp/1000);
459                 }
460                 else if(IS_KEY("Aud_BitRate_Mode"))
461                 {
462                         if(!value.compare("CBR", Qt::CaseInsensitive)) audioFile.techInfo().setAudioBitrateMode(AudioFileModel::BitrateModeConstant);
463                         if(!value.compare("VBR", Qt::CaseInsensitive)) audioFile.techInfo().setAudioBitrateMode(AudioFileModel::BitrateModeVariable);
464                 }
465                 else if(IS_KEY("Aud_Encoded_Library"))
466                 {
467                         audioFile.techInfo().setAudioEncodeLib(value);
468                 }
469                 else
470                 {
471                         qWarning("Unknown key '%s' with value '%s' found!", MUTILS_UTF8(key), MUTILS_UTF8(value));
472                 }
473                 return;
474         }
475
476         /*Section not recognized*/
477         qWarning("Unknown section: %s", MUTILS_UTF8(key));
478 }
479
480 bool AnalyzeTask::checkFile_CDDA(QFile &file)
481 {
482         file.reset();
483         QByteArray data = file.read(128);
484         
485         int i = data.indexOf("RIFF");
486         int j = data.indexOf("CDDA");
487         int k = data.indexOf("fmt ");
488
489         return ((i >= 0) && (j >= 0) && (k >= 0) && (k > j) && (j > i));
490 }
491
492 void AnalyzeTask::retrieveCover(AudioFileModel &audioFile, const quint32 coverType, const QByteArray &coverData)
493 {
494         qDebug("Retrieving cover! (MIME_TYPES_MAX=%u)", MIME_TYPES_MAX);
495         
496         static const QString ext = QString::fromLatin1(MIME_TYPES[qBound(0U, coverType, MIME_TYPES_MAX)].ext[0]);
497         if(!(QImage::fromData(coverData, ext.toUpper().toLatin1().constData()).isNull()))
498         {
499                 QFile coverFile(QString("%1/%2.%3").arg(MUtils::temp_folder(), MUtils::rand_str(), ext));
500                 if(coverFile.open(QIODevice::WriteOnly))
501                 {
502                         coverFile.write(coverData);
503                         coverFile.close();
504                         audioFile.metaInfo().setCover(coverFile.fileName(), true);
505                 }
506         }
507         else
508         {
509                 qWarning("Image data seems to be invalid :-(");
510         }
511 }
512
513 bool AnalyzeTask::analyzeAvisynthFile(const QString &filePath, AudioFileModel &info)
514 {
515         QProcess process;
516         MUtils::init_process(process, QFileInfo(m_avs2wavBin).absolutePath());
517
518         process.start(m_avs2wavBin, QStringList() << QDir::toNativeSeparators(filePath) << "?");
519
520         if(!process.waitForStarted())
521         {
522                 qWarning("AVS2WAV process failed to create!");
523                 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
524                 process.kill();
525                 process.waitForFinished(-1);
526                 return false;
527         }
528
529         bool bInfoHeaderFound = false;
530
531         while(process.state() != QProcess::NotRunning)
532         {
533                 if(*m_abortFlag)
534                 {
535                         process.kill();
536                         qWarning("Process was aborted on user request!");
537                         break;
538                 }
539                 
540                 if(!process.waitForReadyRead())
541                 {
542                         if(process.state() == QProcess::Running)
543                         {
544                                 qWarning("AVS2WAV time out. Killing process and skipping file!");
545                                 process.kill();
546                                 process.waitForFinished(-1);
547                                 return false;
548                         }
549                 }
550
551                 QByteArray data;
552
553                 while(process.canReadLine())
554                 {
555                         QString line = QString::fromUtf8(process.readLine().constData()).simplified();
556                         if(!line.isEmpty())
557                         {
558                                 int index = line.indexOf(':');
559                                 if(index > 0)
560                                 {
561                                         QString key = line.left(index).trimmed();
562                                         QString val = line.mid(index+1).trimmed();
563
564                                         if(bInfoHeaderFound && !key.isEmpty() && !val.isEmpty())
565                                         {
566                                                 if(key.compare("TotalSeconds", Qt::CaseInsensitive) == 0)
567                                                 {
568                                                         bool ok = false;
569                                                         unsigned int duration = val.toUInt(&ok);
570                                                         if(ok) info.techInfo().setDuration(duration);
571                                                 }
572                                                 if(key.compare("SamplesPerSec", Qt::CaseInsensitive) == 0)
573                                                 {
574                                                         bool ok = false;
575                                                         unsigned int samplerate = val.toUInt(&ok);
576                                                         if(ok) info.techInfo().setAudioSamplerate (samplerate);
577                                                 }
578                                                 if(key.compare("Channels", Qt::CaseInsensitive) == 0)
579                                                 {
580                                                         bool ok = false;
581                                                         unsigned int channels = val.toUInt(&ok);
582                                                         if(ok) info.techInfo().setAudioChannels(channels);
583                                                 }
584                                                 if(key.compare("BitsPerSample", Qt::CaseInsensitive) == 0)
585                                                 {
586                                                         bool ok = false;
587                                                         unsigned int bitdepth = val.toUInt(&ok);
588                                                         if(ok) info.techInfo().setAudioBitdepth(bitdepth);
589                                                 }
590                                         }
591                                 }
592                                 else
593                                 {
594                                         if(line.contains("[Audio Info]", Qt::CaseInsensitive))
595                                         {
596                                                 info.techInfo().setAudioType("Avisynth");
597                                                 info.techInfo().setContainerType("Avisynth");
598                                                 bInfoHeaderFound = true;
599                                         }
600                                 }
601                         }
602                 }
603         }
604         
605         process.waitForFinished();
606         if(process.state() != QProcess::NotRunning)
607         {
608                 process.kill();
609                 process.waitForFinished(-1);
610         }
611
612         //Check exit code
613         switch(process.exitCode())
614         {
615         case 0:
616                 qDebug("Avisynth script was analyzed successfully.");
617                 return true;
618                 break;
619         case -5:
620                 qWarning("It appears that Avisynth is not installed on the system!");
621                 return false;
622                 break;
623         default:
624                 qWarning("Failed to open the Avisynth script, bad AVS file?");
625                 return false;
626                 break;
627         }
628 }
629
630 unsigned int AnalyzeTask::parseYear(const QString &str)
631 {
632         if(str.startsWith("UTC", Qt::CaseInsensitive))
633         {
634                 QDate date = QDate::fromString(str.mid(3).trimmed().left(10), "yyyy-MM-dd");
635                 if(date.isValid())
636                 {
637                         return date.year();
638                 }
639                 else
640                 {
641                         return 0;
642                 }
643         }
644         else
645         {
646                 bool ok = false;
647                 int year = str.toInt(&ok);
648                 if(ok && year > 0)
649                 {
650                         return year;
651                 }
652                 else
653                 {
654                         return 0;
655                 }
656         }
657 }
658
659 unsigned int AnalyzeTask::parseDuration(const QString &str)
660 {
661         bool ok = false;
662         unsigned int value = str.toUInt(&ok);
663         return ok ? (value/1000) : 0;
664 }
665
666
667 ////////////////////////////////////////////////////////////
668 // Public Functions
669 ////////////////////////////////////////////////////////////
670
671 /*NONE*/
672
673 ////////////////////////////////////////////////////////////
674 // EVENTS
675 ////////////////////////////////////////////////////////////
676
677 /*NONE*/