OSDN Git Service

Added support for playlist import (M3U, PLS, ASX and WPL).
[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
28 #include <QDir>
29 #include <QFileInfo>
30 #include <QProcess>
31 #include <QDate>
32 #include <QTime>
33 #include <QDebug>
34
35 #include <math.h>
36
37 ////////////////////////////////////////////////////////////
38 // Constructor
39 ////////////////////////////////////////////////////////////
40
41 FileAnalyzer::FileAnalyzer(const QStringList &inputFiles)
42 :
43         m_inputFiles(inputFiles),
44         m_mediaInfoBin_x86(lamexp_lookup_tool("mediainfo_i386.exe")),
45         m_mediaInfoBin_x64(lamexp_lookup_tool("mediainfo_x64.exe"))
46 {
47         m_bSuccess = false;
48         
49         if(m_mediaInfoBin_x86.isEmpty() || m_mediaInfoBin_x64.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 }
58
59 ////////////////////////////////////////////////////////////
60 // Thread Main
61 ////////////////////////////////////////////////////////////
62
63 void FileAnalyzer::run()
64 {
65         m_bSuccess = false;
66
67         m_filesAccepted = 0;
68         m_filesRejected = 0;
69         m_filesDenied = 0;
70
71         m_inputFiles.sort();
72
73         while(!m_inputFiles.isEmpty())
74         {
75                 QString currentFile = QDir::fromNativeSeparators(m_inputFiles.takeFirst());
76                 qDebug("Analyzing: %s", currentFile.toUtf8().constData());
77                 emit fileSelected(QFileInfo(currentFile).fileName());
78                 AudioFileModel file = analyzeFile(currentFile);
79                 if(file.fileName().isEmpty() || file.formatContainerType().isEmpty() || file.formatAudioType().isEmpty())
80                 {
81                         if(!importPlaylist(m_inputFiles, currentFile))
82                         {
83                                 m_filesRejected++;
84                                 qDebug("Skipped: %s", file.filePath().toUtf8().constData());
85                         }
86                         continue;
87                 }
88                 m_filesAccepted++;
89                 emit fileAnalyzed(file);
90         }
91
92         qDebug("All files added.\n");
93         m_bSuccess = true;
94 }
95
96 ////////////////////////////////////////////////////////////
97 // Privtae Functions
98 ////////////////////////////////////////////////////////////
99
100 const AudioFileModel FileAnalyzer::analyzeFile(const QString &filePath)
101 {
102         lamexp_cpu_t cpuInfo = lamexp_detect_cpu_features();
103         const QString mediaInfoBin = cpuInfo.x64 ? m_mediaInfoBin_x64 : m_mediaInfoBin_x86;
104         
105         AudioFileModel audioFile(filePath);
106         m_currentSection = sectionOther;
107
108         QFile readTest(filePath);
109         if(!readTest.open(QIODevice::ReadOnly))
110         {
111                 qWarning("Cannot access file for reading, skipping!");
112                 m_filesDenied++;
113                 return audioFile;
114         }
115         else
116         {
117                 readTest.close();
118         }
119
120         QProcess process;
121         process.setProcessChannelMode(QProcess::MergedChannels);
122         process.setReadChannel(QProcess::StandardOutput);
123         process.start(mediaInfoBin, QStringList() << QDir::toNativeSeparators(filePath));
124         
125         if(!process.waitForStarted())
126         {
127                 qWarning("MediaInfo process failed to create!");
128                 qWarning("Error message: \"%s\"\n", process.errorString().toLatin1().constData());
129                 process.kill();
130                 process.waitForFinished(-1);
131                 return audioFile;
132         }
133
134         while(process.state() != QProcess::NotRunning)
135         {
136                 if(!process.waitForReadyRead())
137                 {
138                         if(process.state() == QProcess::Running)
139                         {
140                                 qWarning("MediaInfo time out. Killing process and skipping file!");
141                                 process.kill();
142                                 process.waitForFinished(-1);
143                                 return audioFile;
144                         }
145                 }
146
147                 QByteArray data;
148
149                 while(process.canReadLine())
150                 {
151                         QString line = QString::fromUtf8(process.readLine().constData()).simplified();
152                         if(!line.isEmpty())
153                         {
154                                 int index = line.indexOf(':');
155                                 if(index > 0)
156                                 {
157                                         QString key = line.left(index-1).trimmed();
158                                         QString val = line.mid(index+1).trimmed();
159                                         if(!key.isEmpty() && !val.isEmpty())
160                                         {
161                                                 updateInfo(audioFile, key, val);
162                                         }
163                                 }
164                                 else
165                                 {
166                                         updateSection(line);
167                                 }
168                         }
169                 }
170         }
171
172         if(audioFile.fileName().isEmpty())
173         {
174                 QString baseName = QFileInfo(filePath).fileName();
175                 int index = baseName.lastIndexOf(".");
176
177                 if(index >= 0)
178                 {
179                         baseName = baseName.left(index);
180                 }
181
182                 baseName = baseName.replace("_", " ").simplified();
183                 index = baseName.lastIndexOf(" - ");
184
185                 if(index >= 0)
186                 {
187                         baseName = baseName.mid(index + 3).trimmed();
188                 }
189
190                 audioFile.setFileName(baseName);
191         }
192         
193         return audioFile;
194 }
195
196 void FileAnalyzer::updateSection(const QString &section)
197 {
198         if(section.startsWith("General", Qt::CaseInsensitive))
199         {
200                 m_currentSection = sectionGeneral;
201         }
202         else if(!section.compare("Audio", Qt::CaseInsensitive) || section.startsWith("Audio #1", Qt::CaseInsensitive))
203         {
204                 m_currentSection = sectionAudio;
205         }
206         else if(section.startsWith("Audio", Qt::CaseInsensitive) || section.startsWith("Video", Qt::CaseInsensitive) || section.startsWith("Text", Qt::CaseInsensitive) ||
207                 section.startsWith("Menu", Qt::CaseInsensitive) || section.startsWith("Image", Qt::CaseInsensitive) || section.startsWith("Chapters", Qt::CaseInsensitive))
208         {
209                 m_currentSection = sectionOther;
210         }
211         else
212         {
213                 qWarning("Unknown section: %s", section.toUtf8().constData());
214         }
215 }
216
217 void FileAnalyzer::updateInfo(AudioFileModel &audioFile, const QString &key, const QString &value)
218 {
219         switch(m_currentSection)
220         {
221         case sectionGeneral:
222                 if(!key.compare("Title", Qt::CaseInsensitive) || !key.compare("Track", Qt::CaseInsensitive) || !key.compare("Track Name", Qt::CaseInsensitive))
223                 {
224                         if(audioFile.fileName().isEmpty()) audioFile.setFileName(value);
225                 }
226                 else if(!key.compare("Duration", Qt::CaseInsensitive))
227                 {
228                         if(!audioFile.fileDuration()) audioFile.setFileDuration(parseDuration(value));
229                 }
230                 else if(!key.compare("Artist", Qt::CaseInsensitive) || !key.compare("Performer", Qt::CaseInsensitive))
231                 {
232                         if(audioFile.fileArtist().isEmpty()) audioFile.setFileArtist(value);
233                 }
234                 else if(!key.compare("Album", Qt::CaseInsensitive))
235                 {
236                         if(audioFile.fileAlbum().isEmpty()) audioFile.setFileAlbum(value);
237                 }
238                 else if(!key.compare("Genre", Qt::CaseInsensitive))
239                 {
240                         if(audioFile.fileGenre().isEmpty()) audioFile.setFileGenre(value);
241                 }
242                 else if(!key.compare("Year", Qt::CaseInsensitive) || !key.compare("Recorded Date", Qt::CaseInsensitive) || !key.compare("Encoded Date", Qt::CaseInsensitive))
243                 {
244                         if(!audioFile.fileYear()) audioFile.setFileYear(parseYear(value));
245                 }
246                 else if(!key.compare("Comment", Qt::CaseInsensitive))
247                 {
248                         if(audioFile.fileComment().isEmpty()) audioFile.setFileComment(value);
249                 }
250                 else if(!key.compare("Track Name/Position", Qt::CaseInsensitive))
251                 {
252                         if(!audioFile.filePosition()) audioFile.setFilePosition(value.toInt());
253                 }
254                 else if(!key.compare("Format", Qt::CaseInsensitive))
255                 {
256                         if(audioFile.formatContainerType().isEmpty()) audioFile.setFormatContainerType(value);
257                 }
258                 else if(!key.compare("Format Profile", Qt::CaseInsensitive))
259                 {
260                         if(audioFile.formatContainerProfile().isEmpty()) audioFile.setFormatContainerProfile(value);
261                 }
262                 break;
263
264         case sectionAudio:
265                 if(!key.compare("Year", Qt::CaseInsensitive) || !key.compare("Recorded Date", Qt::CaseInsensitive) || !key.compare("Encoded Date", Qt::CaseInsensitive))
266                 {
267                         if(!audioFile.fileYear()) audioFile.setFileYear(parseYear(value));
268                 }
269                 else if(!key.compare("Format", Qt::CaseInsensitive))
270                 {
271                         if(audioFile.formatAudioType().isEmpty()) audioFile.setFormatAudioType(value);
272                 }
273                 else if(!key.compare("Format Profile", Qt::CaseInsensitive))
274                 {
275                         if(audioFile.formatAudioProfile().isEmpty()) audioFile.setFormatAudioProfile(value);
276                 }
277                 else if(!key.compare("Format Version", Qt::CaseInsensitive))
278                 {
279                         if(audioFile.formatAudioVersion().isEmpty()) audioFile.setFormatAudioVersion(value);
280                 }
281                 else if(!key.compare("Channel(s)", Qt::CaseInsensitive))
282                 {
283                         if(!audioFile.formatAudioChannels()) audioFile.setFormatAudioChannels(value.split(" ", QString::SkipEmptyParts).first().toInt());
284                 }
285                 else if(!key.compare("Sampling rate", Qt::CaseInsensitive))
286                 {
287                         if(!audioFile.formatAudioSamplerate()) audioFile.setFormatAudioSamplerate(ceil(value.split(" ", QString::SkipEmptyParts).first().toFloat() * 1000.0f));
288                 }
289                 else if(!key.compare("Bit depth", Qt::CaseInsensitive))
290                 {
291                         if(!audioFile.formatAudioBitdepth()) audioFile.setFormatAudioBitdepth(value.split(" ", QString::SkipEmptyParts).first().toInt());
292                 }
293                 else if(!key.compare("Duration", Qt::CaseInsensitive))
294                 {
295                         if(!audioFile.fileDuration()) audioFile.setFileDuration(parseDuration(value));
296                 }
297                 break;
298         }
299 }
300
301 unsigned int FileAnalyzer::parseYear(const QString &str)
302 {
303         if(str.startsWith("UTC", Qt::CaseInsensitive))
304         {
305                 QDate date = QDate::fromString(str.mid(3).trimmed().left(10), "yyyy-MM-dd");
306                 if(date.isValid())
307                 {
308                         return date.year();
309                 }
310                 else
311                 {
312                         return 0;
313                 }
314         }
315         else
316         {
317                 bool ok = false;
318                 int year = str.toInt(&ok);
319                 if(ok && year > 0)
320                 {
321                         return year;
322                 }
323                 else
324                 {
325                         return 0;
326                 }
327         }
328 }
329
330 unsigned int FileAnalyzer::parseDuration(const QString &str)
331 {
332         QTime time;
333
334         time = QTime::fromString(str, "z'ms'");
335         if(time.isValid())
336         {
337                 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
338         }
339
340         time = QTime::fromString(str, "s's 'z'ms'");
341         if(time.isValid())
342         {
343                 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
344         }
345
346         time = QTime::fromString(str, "m'mn 's's'");
347         if(time.isValid())
348         {
349                 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
350         }
351
352         time = QTime::fromString(str, "h'h 'm'mn'");
353         if(time.isValid())
354         {
355                 return max(1, (time.hour() * 60 * 60) + (time.minute() * 60) + time.second());
356         }
357
358         return 0;
359 }
360
361
362 bool FileAnalyzer::importPlaylist(QStringList &fileList, const QString &playlistFile)
363 {
364         QFileInfo file(playlistFile);
365         QDir baseDir(file.canonicalPath());
366
367         QDir rootDir(baseDir);
368         while(rootDir.cdUp());
369
370         //Sanity check
371         if(file.size() < 3 || file.size() > 512000)
372         {
373                 return false;
374         }
375         
376         //Detect playlist type
377         playlist_t playlistType = isPlaylist(file.canonicalFilePath());
378
379         //Exit if not a playlist
380         if(playlistType == noPlaylist)
381         {
382                 return false;
383         }
384         
385         QFile data(playlistFile);
386
387         //Open file for reading
388         if(!data.open(QIODevice::ReadOnly))
389         {
390                 return false;
391         }
392
393         //Parse playlist depending on type
394         switch(playlistType)
395         {
396         case m3uPlaylist:
397                 return parsePlaylist_m3u(data, fileList, baseDir, rootDir);
398                 break;
399         case plsPlaylist:
400                 return parsePlaylist_pls(data, fileList, baseDir, rootDir);
401                 break;
402         case wplPlaylist:
403                 return parsePlaylist_wpl(data, fileList, baseDir, rootDir);
404                 break;
405         default:
406                 return false;
407                 break;
408         }
409 }
410
411 bool FileAnalyzer::parsePlaylist_m3u(QFile &data, QStringList &fileList, const QDir &baseDir, const QDir &rootDir)
412 {
413         QByteArray line = data.readLine();
414         
415         while(line.size() > 0)
416         {
417                 QFileInfo filename1(QDir::fromNativeSeparators(QString::fromUtf8(line.constData(), line.size()).trimmed()));
418                 QFileInfo filename2(QDir::fromNativeSeparators(QString::fromLatin1(line.constData(), line.size()).trimmed()));
419
420                 filename1.setCaching(false);
421                 filename2.setCaching(false);
422
423                 if(!(filename1.filePath().startsWith("#") || filename2.filePath().startsWith("#")))
424                 {
425                         fixFilePath(filename1, baseDir, rootDir);
426                         fixFilePath(filename2, baseDir, rootDir);
427
428                         if(filename1.exists())
429                         {
430                                 if(isPlaylist(filename1.canonicalFilePath()) == noPlaylist)
431                                 {
432                                         fileList << filename1.canonicalFilePath();
433                                 }
434                         }
435                         else if(filename2.exists())
436                         {
437                                 if(isPlaylist(filename2.canonicalFilePath()) == noPlaylist)
438                                 {
439                                         fileList << filename2.canonicalFilePath();
440                                 }
441                         }
442                 }
443
444                 line = data.readLine();
445         }
446
447         return true;
448 }
449
450 bool FileAnalyzer::parsePlaylist_pls(QFile &data, QStringList &fileList, const QDir &baseDir, const QDir &rootDir)
451 {
452         QRegExp plsEntry("File(\\d+)=(.+)", Qt::CaseInsensitive);
453         QByteArray line = data.readLine();
454         
455         while(line.size() > 0)
456         {
457                 bool flag = false;
458                 
459                 QString temp1(QDir::fromNativeSeparators(QString::fromUtf8(line.constData(), line.size()).trimmed()));
460                 QString temp2(QDir::fromNativeSeparators(QString::fromLatin1(line.constData(), line.size()).trimmed()));
461
462                 if(!flag && plsEntry.indexIn(temp1) >= 0)
463                 {
464                         QFileInfo filename(QDir::fromNativeSeparators(plsEntry.cap(2)).trimmed());
465                         filename.setCaching(false);
466                         fixFilePath(filename, baseDir, rootDir);
467
468                         if(filename.exists())
469                         {
470                                 if(isPlaylist(filename.canonicalFilePath()) == noPlaylist)
471                                 {
472                                         fileList << filename.canonicalFilePath();
473                                         flag = true;
474                                 }
475                         }
476                 }
477                 
478                 if(!flag && plsEntry.indexIn(temp2) >= 0)
479                 {
480                         QFileInfo filename(QDir::fromNativeSeparators(plsEntry.cap(2)).trimmed());
481                         filename.setCaching(false);
482                         fixFilePath(filename, baseDir, rootDir);
483
484                         if(filename.exists())
485                         {
486                                 if(isPlaylist(filename.canonicalFilePath()) == noPlaylist)
487                                 {
488                                         fileList << filename.canonicalFilePath();
489                                         flag = true;
490                                 }
491                         }
492                 }
493
494                 line = data.readLine();
495         }
496
497         return true;
498 }
499
500 bool FileAnalyzer::parsePlaylist_wpl(QFile &data, QStringList &fileList, const QDir &baseDir, const QDir &rootDir)
501 {
502         QRegExp wplEntry("<(media|ref)[^<>]*(src|href)=\"([^\"]+)\"[^<>]*>", Qt::CaseInsensitive);
503         QByteArray line = data.readLine();
504         
505         while(line.size() > 0)
506         {
507                 bool flag = false;
508                 
509                 QString temp1(QDir::fromNativeSeparators(QString::fromUtf8(line.constData(), line.size()).trimmed()));
510                 QString temp2(QDir::fromNativeSeparators(QString::fromLatin1(line.constData(), line.size()).trimmed()));
511
512                 if(!flag && wplEntry.indexIn(temp1) >= 0)
513                 {
514                         QFileInfo filename(QDir::fromNativeSeparators(wplEntry.cap(3)).trimmed());
515                         filename.setCaching(false);
516                         fixFilePath(filename, baseDir, rootDir);
517
518                         if(filename.exists())
519                         {
520                                 if(isPlaylist(filename.canonicalFilePath()) == noPlaylist)
521                                 {
522                                         fileList << filename.canonicalFilePath();
523                                         flag = true;
524                                 }
525                         }
526                 }
527                 
528                 if(!flag && wplEntry.indexIn(temp2) >= 0)
529                 {
530                         QFileInfo filename(QDir::fromNativeSeparators(wplEntry.cap(3)).trimmed());
531                         filename.setCaching(false);
532                         fixFilePath(filename, baseDir, rootDir);
533
534                         if(filename.exists())
535                         {
536                                 if(isPlaylist(filename.canonicalFilePath()) == noPlaylist)
537                                 {
538                                         fileList << filename.canonicalFilePath();
539                                         flag = true;
540                                 }
541                         }
542                 }
543
544                 line = data.readLine();
545         }
546
547         return true;
548 }
549
550 FileAnalyzer::playlist_t FileAnalyzer::isPlaylist(const QString &fileName)
551 {
552         QFileInfo file (fileName);
553         
554         if(file.suffix().compare("m3u", Qt::CaseInsensitive) == 0)
555         {
556                 return m3uPlaylist;
557         }
558         else if(file.suffix().compare("m3u8", Qt::CaseInsensitive) == 0)
559         {
560                 return m3uPlaylist;
561         }
562         else if(file.suffix().compare("pls", Qt::CaseInsensitive) == 0)
563         {
564                 return  plsPlaylist;
565         }
566         else if(file.suffix().compare("asx", Qt::CaseInsensitive) == 0)
567         {
568                 return  wplPlaylist;
569         }
570         else if(file.suffix().compare("wpl", Qt::CaseInsensitive) == 0)
571         {
572                 return  wplPlaylist;
573         }
574         else
575         {
576                 return noPlaylist;
577         }
578 }
579
580 void FileAnalyzer::fixFilePath(QFileInfo &filename, const QDir &baseDir, const QDir &rootDir)
581 {
582         if(filename.filePath().startsWith("/"))
583         {
584                 while(filename.filePath().startsWith("/"))
585                 {
586                         filename.setFile(filename.filePath().mid(1));
587                 }
588                 filename.setFile(rootDir.filePath(filename.filePath()));
589         }
590         
591         if(!filename.isAbsolute())
592         {
593                 filename.setFile(baseDir.filePath(filename.filePath()));
594         }
595 }
596
597 ////////////////////////////////////////////////////////////
598 // Public Functions
599 ////////////////////////////////////////////////////////////
600
601 unsigned int FileAnalyzer::filesAccepted(void)
602 {
603         return m_filesAccepted;
604 }
605
606 unsigned int FileAnalyzer::filesRejected(void)
607 {
608         return m_filesRejected - m_filesDenied;
609 }
610
611 unsigned int FileAnalyzer::filesDenied(void)
612 {
613         return m_filesDenied;
614 }
615
616 ////////////////////////////////////////////////////////////
617 // EVENTS
618 ////////////////////////////////////////////////////////////
619
620 /*NONE*/