OSDN Git Service

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