OSDN Git Service

Moved Natural String Sort functions into MUtils library + moved the "Prerequisites...
[lamexp/LameXP.git] / src / Thread_FileAnalyzer.cpp
1 ///////////////////////////////////////////////////////////////////////////////
2 // LameXP - Audio Encoder Front-End
3 // Copyright (C) 2004-2014 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.h"
24
25 //Internal
26 #include "Global.h"
27 #include "LockedFile.h"
28 #include "Model_AudioFile.h"
29 #include "Thread_FileAnalyzer_Task.h"
30 #include "PlaylistImporter.h"
31
32 //MUtils
33 #include <MUtils/Global.h>
34
35 //Qt
36 #include <QDir>
37 #include <QFileInfo>
38 #include <QProcess>
39 #include <QDate>
40 #include <QTime>
41 #include <QDebug>
42 #include <QImage>
43 #include <QThreadPool>
44 #include <QTime>
45 #include <QElapsedTimer>
46 #include <QTimer>
47 #include <QQueue>
48
49 //Insert into QStringList *without* duplicates
50 static inline void SAFE_APPEND_STRING(QStringList &list, const QString &str)
51 {
52         if(!list.contains(str, Qt::CaseInsensitive))
53         {
54                 list << str;
55         }
56 }
57
58 ////////////////////////////////////////////////////////////
59 // Constructor
60 ////////////////////////////////////////////////////////////
61
62 FileAnalyzer::FileAnalyzer(const QStringList &inputFiles)
63 :
64         m_tasksCounterNext(0),
65         m_tasksCounterDone(0),
66         m_inputFiles(inputFiles),
67         m_templateFile(NULL),
68         m_pool(NULL)
69 {
70         m_bSuccess = false;
71         m_bAborted = false;
72
73         m_filesAccepted = 0;
74         m_filesRejected = 0;
75         m_filesDenied = 0;
76         m_filesDummyCDDA = 0;
77         m_filesCueSheet = 0;
78
79         moveToThread(this); /*makes sure queued slots are executed in the proper thread context*/
80
81         m_timer = new QElapsedTimer;
82 }
83
84 FileAnalyzer::~FileAnalyzer(void)
85 {
86         if(m_pool)
87         {
88                 if(!m_pool->waitForDone(2500))
89                 {
90                         qWarning("There are still running tasks in the thread pool!");
91                 }
92         }
93
94         MUTILS_DELETE(m_templateFile);
95         MUTILS_DELETE(m_pool);
96         MUTILS_DELETE(m_timer);
97 }
98
99 ////////////////////////////////////////////////////////////
100 // Static data
101 ////////////////////////////////////////////////////////////
102
103 const char *FileAnalyzer::g_tags_gen[] =
104 {
105         "ID",
106         "Format",
107         "Format_Profile",
108         "Format_Version",
109         "Duration",
110         "Title", "Track",
111         "Track/Position",
112         "Artist", "Performer",
113         "Album",
114         "Genre",
115         "Released_Date", "Recorded_Date",
116         "Comment",
117         "Cover",
118         "Cover_Type",
119         "Cover_Mime",
120         "Cover_Data",
121         NULL
122 };
123
124 const char *FileAnalyzer::g_tags_aud[] =
125 {
126         "ID",
127         "Source",
128         "Format",
129         "Format_Profile",
130         "Format_Version",
131         "Channel(s)",
132         "SamplingRate",
133         "BitDepth",
134         "BitRate",
135         "BitRate_Mode",
136         "Encoded_Library",
137         NULL
138 };
139
140 ////////////////////////////////////////////////////////////
141 // Thread Main
142 ////////////////////////////////////////////////////////////
143
144 void FileAnalyzer::run()
145 {
146         m_bSuccess = false;
147
148         m_tasksCounterNext = 0;
149         m_tasksCounterDone = 0;
150         m_completedCounter = 0;
151
152         m_completedFiles.clear();
153         m_completedTaskIds.clear();
154         m_runningTaskIds.clear();
155
156         m_filesAccepted = 0;
157         m_filesRejected = 0;
158         m_filesDenied = 0;
159         m_filesDummyCDDA = 0;
160         m_filesCueSheet = 0;
161
162         m_timer->invalidate();
163
164         //Create MediaInfo template file
165         if(!m_templateFile)
166         {
167                 if(!createTemplate())
168                 {
169                         qWarning("Failed to create template file!");
170                         return;
171                 }
172         }
173
174         //Sort files
175         MUtils::natural_string_sort(m_inputFiles, true);
176
177         //Handle playlist files first!
178         handlePlaylistFiles();
179
180         const unsigned int nFiles = m_inputFiles.count();
181         if(nFiles < 1)
182         {
183                 qWarning("File list is empty, nothing to do!");
184                 return;
185         }
186
187         //Update progress
188         emit progressMaxChanged(nFiles);
189         emit progressValChanged(0);
190
191         //Create thread pool
192         if(!m_pool) m_pool = new QThreadPool();
193         const int idealThreadCount = QThread::idealThreadCount();
194         if(idealThreadCount > 0)
195         {
196                 m_pool->setMaxThreadCount(qBound(2, ((idealThreadCount * 3) / 2), 12));
197         }
198
199         //Start first N threads
200         QTimer::singleShot(0, this, SLOT(initializeTasks()));
201
202         //Start event processing
203         this->exec();
204
205         //Wait for pending tasks to complete
206         m_pool->waitForDone();
207
208         //Was opertaion aborted?
209         if(m_bAborted)
210         {
211                 qWarning("Operation cancelled by user!");
212                 return;
213         }
214         
215         //Update progress
216         emit progressValChanged(nFiles);
217
218         //Emit pending files (this should not be required though!)
219         if(!m_completedFiles.isEmpty())
220         {
221                 qWarning("FileAnalyzer: Pending file information found after last thread terminated!");
222                 QList<unsigned int> keys = m_completedFiles.keys(); qSort(keys);
223                 while(!keys.isEmpty())
224                 {
225                         emit fileAnalyzed(m_completedFiles.take(keys.takeFirst()));
226                 }
227         }
228
229         qDebug("All files added.\n");
230         m_bSuccess = true;
231
232         QThread::msleep(333);
233 }
234
235 ////////////////////////////////////////////////////////////
236 // Privtae Functions
237 ////////////////////////////////////////////////////////////
238
239 bool FileAnalyzer::analyzeNextFile(void)
240 {
241         if(!(m_inputFiles.isEmpty() || m_bAborted))
242         {
243                 const unsigned int taskId = m_tasksCounterNext++;
244                 const QString currentFile = QDir::fromNativeSeparators(m_inputFiles.takeFirst());
245
246                 if((!m_timer->isValid()) || (m_timer->elapsed() >= 333))
247                 {
248                         emit fileSelected(QFileInfo(currentFile).fileName());
249                         m_timer->restart();
250                 }
251         
252                 AnalyzeTask *task = new AnalyzeTask(taskId, currentFile, m_templateFile->filePath(), &m_bAborted);
253                 connect(task, SIGNAL(fileAnalyzed(const unsigned int, const int, AudioFileModel)), this, SLOT(taskFileAnalyzed(unsigned int, const int, AudioFileModel)), Qt::QueuedConnection);
254                 connect(task, SIGNAL(taskCompleted(const unsigned int)), this, SLOT(taskThreadFinish(const unsigned int)), Qt::QueuedConnection);
255                 m_runningTaskIds.insert(taskId); m_pool->start(task);
256
257                 return true;
258         }
259
260         return false;
261 }
262
263 void FileAnalyzer::handlePlaylistFiles(void)
264 {
265         QQueue<QVariant> queue;
266         QStringList importedFromPlaylist;
267         
268         //Import playlist files into "hierarchical" list
269         while(!m_inputFiles.isEmpty())
270         {
271                 const QString currentFile = m_inputFiles.takeFirst();
272                 QStringList importedFiles;
273                 if(PlaylistImporter::importPlaylist(importedFiles, currentFile))
274                 {
275                         queue.enqueue(importedFiles);
276                         importedFromPlaylist << importedFiles;
277                 }
278                 else
279                 {
280                         queue.enqueue(currentFile);
281                 }
282         }
283
284         //Reduce temporary list
285         importedFromPlaylist.removeDuplicates();
286
287         //Now build the complete "flat" file list (files imported from playlist take precedence!)
288         while(!queue.isEmpty())
289         {
290                 const QVariant current = queue.dequeue();
291                 if(current.type() == QVariant::String)
292                 {
293                         const QString temp = current.toString();
294                         if(!importedFromPlaylist.contains(temp, Qt::CaseInsensitive))
295                         {
296                                 SAFE_APPEND_STRING(m_inputFiles, temp);
297                         }
298                 }
299                 else if(current.type() == QVariant::StringList)
300                 {
301                         const QStringList temp = current.toStringList();
302                         for(QStringList::ConstIterator iter = temp.constBegin(); iter != temp.constEnd(); iter++)
303                         {
304                                 SAFE_APPEND_STRING(m_inputFiles, (*iter));
305                         }
306                 }
307                 else
308                 {
309                         qWarning("Encountered an unexpected variant type!");
310                 }
311         }
312 }
313
314 bool FileAnalyzer::createTemplate(void)
315 {
316         if(m_templateFile)
317         {
318                 qWarning("Template file already exists!");
319                 return true;
320         }
321         
322         QString templatePath = QString("%1/%2.txt").arg(MUtils::temp_folder(), MUtils::rand_str());
323
324         QFile templateFile(templatePath);
325         if(!templateFile.open(QIODevice::WriteOnly))
326         {
327                 return false;
328         }
329
330         templateFile.write("General;");
331         for(size_t i = 0; g_tags_gen[i]; i++)
332         {
333                 templateFile.write(QString("Gen_%1=%%1%\\n").arg(g_tags_gen[i]).toLatin1().constData());
334         }
335         templateFile.write("\\n\r\n");
336
337         templateFile.write("Audio;");
338         for(size_t i = 0; g_tags_aud[i]; i++)
339         {
340                 templateFile.write(QString("Aud_%1=%%1%\\n").arg(g_tags_aud[i]).toLatin1().constData());
341         }
342         templateFile.write("\\n\r\n");
343
344         bool success = (templateFile.error() == QFile::NoError);
345         templateFile.close();
346         
347         if(!success)
348         {
349                 QFile::remove(templatePath);
350                 return false;
351         }
352
353         try
354         {
355                 m_templateFile = new LockedFile(templatePath, true);
356         }
357         catch(const std::exception &error)
358         {
359                 qWarning("Failed to lock template file:\n%s\n", error.what());
360                 return false;
361         }
362         catch(...)
363         {
364                 qWarning("Failed to lock template file!");
365                 return false;
366         }
367
368         return true;
369 }
370
371 ////////////////////////////////////////////////////////////
372 // Slot Functions
373 ////////////////////////////////////////////////////////////
374
375 void FileAnalyzer::initializeTasks(void)
376 {
377         for(int i = 0; i < m_pool->maxThreadCount(); i++)
378         {
379                 if(!analyzeNextFile()) break;
380         }
381 }
382
383 void FileAnalyzer::taskFileAnalyzed(const unsigned int taskId, const int fileType, const AudioFileModel &file)
384 {
385         m_completedTaskIds.insert(taskId);
386
387         switch(fileType)
388         {
389         case AnalyzeTask::fileTypeNormal:
390                 m_filesAccepted++;
391                 if(m_tasksCounterDone == taskId)
392                 {
393                         emit fileAnalyzed(file);
394                         m_tasksCounterDone++;
395                 }
396                 else
397                 {
398                         m_completedFiles.insert(taskId, file);
399                 }
400                 break;
401         case AnalyzeTask::fileTypeCDDA:
402                 m_filesDummyCDDA++;
403                 break;
404         case AnalyzeTask::fileTypeDenied:
405                 m_filesDenied++;
406                 break;
407         case AnalyzeTask::fileTypeCueSheet:
408                 m_filesCueSheet++;
409                 break;
410         case AnalyzeTask::fileTypeUnknown:
411                 m_filesRejected++;
412                 break;
413         default:
414                 THROW("Unknown file type identifier!");
415         }
416
417         //Emit all pending files
418         while(m_completedTaskIds.contains(m_tasksCounterDone))
419         {
420                 if(m_completedFiles.contains(m_tasksCounterDone))
421                 {
422                         emit fileAnalyzed(m_completedFiles.take(m_tasksCounterDone));
423                 }
424                 m_completedTaskIds.remove(m_tasksCounterDone);
425                 m_tasksCounterDone++;
426         }
427 }
428
429 void FileAnalyzer::taskThreadFinish(const unsigned int taskId)
430 {
431         m_runningTaskIds.remove(taskId);
432         emit progressValChanged(++m_completedCounter);
433
434         if(!analyzeNextFile())
435         {
436                 if(m_runningTaskIds.empty())
437                 {
438                         QTimer::singleShot(0, this, SLOT(quit())); //Stop event processing, if all threads have completed!
439                 }
440         }
441 }
442
443 ////////////////////////////////////////////////////////////
444 // Public Functions
445 ////////////////////////////////////////////////////////////
446
447 unsigned int FileAnalyzer::filesAccepted(void)
448 {
449         return m_filesAccepted;
450 }
451
452 unsigned int FileAnalyzer::filesRejected(void)
453 {
454         return m_filesRejected;
455 }
456
457 unsigned int FileAnalyzer::filesDenied(void)
458 {
459         return m_filesDenied;
460 }
461
462 unsigned int FileAnalyzer::filesDummyCDDA(void)
463 {
464         return m_filesDummyCDDA;
465 }
466
467 unsigned int FileAnalyzer::filesCueSheet(void)
468 {
469         return m_filesCueSheet;
470 }
471
472 ////////////////////////////////////////////////////////////
473 // EVENTS
474 ////////////////////////////////////////////////////////////
475
476 /*NONE*/