OSDN Git Service

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