OSDN Git Service

More code refactoring and clean-up.
[lamexp/LameXP.git] / src / Model_FileList.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 "Model_FileList.h"
24
25 //Internal
26 #include "Global.h"
27
28 //MUtils
29 #include <MUtils/Global.h>
30
31 //Qt
32 #include <QFileInfo>
33 #include <QDir>
34 #include <QFile>
35 #include <QTextCodec>
36 #include <QTextStream>
37 #include <QInputDialog>
38
39 #define EXPAND(STR) QString(STR).leftJustified(96, ' ')
40 #define CHECK_HDR(STR,NAM) (!(STR).compare((NAM), Qt::CaseInsensitive))
41 #define MAKE_KEY(PATH) (QDir::fromNativeSeparators(PATH).toLower())
42
43 ////////////////////////////////////////////////////////////
44 // Constructor & Destructor
45 ////////////////////////////////////////////////////////////
46
47 FileListModel::FileListModel(void)
48 :
49         m_blockUpdates(false),
50         m_fileIcon(":/icons/page_white_cd.png")
51 {
52 }
53
54 FileListModel::~FileListModel(void)
55 {
56 }
57
58 ////////////////////////////////////////////////////////////
59 // Public Functions
60 ////////////////////////////////////////////////////////////
61
62 int FileListModel::columnCount(const QModelIndex &parent) const
63 {
64         return 2;
65 }
66
67 int FileListModel::rowCount(const QModelIndex &parent) const
68 {
69         return m_fileList.count();
70 }
71
72 QVariant FileListModel::data(const QModelIndex &index, int role) const
73 {
74         if((role == Qt::DisplayRole || role == Qt::ToolTipRole) && index.row() < m_fileList.count() && index.row() >= 0)
75         {
76                 switch(index.column())
77                 {
78                 case 0:
79                         return m_fileStore.value(m_fileList.at(index.row())).metaInfo().title();
80                         break;
81                 case 1:
82                         return QDir::toNativeSeparators(m_fileStore.value(m_fileList.at(index.row())).filePath());
83                         break;
84                 default:
85                         return QVariant();
86                         break;
87                 }               
88         }
89         else if(role == Qt::DecorationRole && index.column() == 0)
90         {
91                 return m_fileIcon;
92         }
93         else
94         {
95                 return QVariant();
96         }
97 }
98
99 QVariant FileListModel::headerData(int section, Qt::Orientation orientation, int role) const
100 {
101         if(role == Qt::DisplayRole)
102         {
103                 if(orientation == Qt::Horizontal)
104                 {
105                         switch(section)
106                         {
107                         case 0:
108                                 return QVariant(tr("Title"));
109                                 break;
110                         case 1:
111                                 return QVariant(tr("Full Path"));
112                                 break;
113                         default:
114                                 return QVariant();
115                                 break;
116                         }
117                 }
118                 else
119                 {
120                         if(m_fileList.count() < 10)
121                         {
122                                 return QVariant(QString().sprintf("%d", section + 1));
123                         }
124                         else if(m_fileList.count() < 100)
125                         {
126                                 return QVariant(QString().sprintf("%02d", section + 1));
127                         }
128                         else if(m_fileList.count() < 1000)
129                         {
130                                 return QVariant(QString().sprintf("%03d", section + 1));
131                         }
132                         else
133                         {
134                                 return QVariant(QString().sprintf("%04d", section + 1));
135                         }
136                 }
137         }
138         else
139         {
140                 return QVariant();
141         }
142 }
143
144 void FileListModel::addFile(const QString &filePath)
145 {
146         QFileInfo fileInfo(filePath);
147         const QString key = MAKE_KEY(fileInfo.canonicalFilePath()); 
148         const bool flag = (!m_blockUpdates);
149
150         if(!m_fileStore.contains(key))
151         {
152                 AudioFileModel audioFile(fileInfo.canonicalFilePath());
153                 audioFile.metaInfo().setTitle(fileInfo.baseName());
154                 if(flag) beginInsertRows(QModelIndex(), m_fileList.count(), m_fileList.count());
155                 m_fileStore.insert(key, audioFile);
156                 m_fileList.append(key);
157                 if(flag) endInsertRows();
158                 emit rowAppended();
159         }
160 }
161
162 void FileListModel::addFile(const AudioFileModel &file)
163 {
164         const QString key = MAKE_KEY(file.filePath()); 
165         const bool flag = (!m_blockUpdates);
166
167         if(!m_fileStore.contains(key))
168         {
169                 if(flag) beginInsertRows(QModelIndex(), m_fileList.count(), m_fileList.count());
170                 m_fileStore.insert(key, file);
171                 m_fileList.append(key);
172                 if(flag) endInsertRows();
173                 emit rowAppended();
174         }
175 }
176
177 bool FileListModel::removeFile(const QModelIndex &index)
178 {
179         if(index.row() >= 0 && index.row() < m_fileList.count())
180         {
181                 beginResetModel();
182                 m_fileStore.remove(m_fileList.at(index.row()));
183                 m_fileList.removeAt(index.row());
184                 endResetModel();
185                 return true;
186         }
187         else
188         {
189                 return false;
190         }
191 }
192
193 void FileListModel::clearFiles(void)
194 {
195         beginResetModel();
196         m_fileList.clear();
197         m_fileStore.clear();
198         endResetModel();
199 }
200
201 bool FileListModel::moveFile(const QModelIndex &index, int delta)
202 {
203         if(delta != 0 && index.row() >= 0 && index.row() < m_fileList.count() && index.row() + delta >= 0 && index.row() + delta < m_fileList.count())
204         {
205                 beginResetModel();
206                 m_fileList.move(index.row(), index.row() + delta);
207                 endResetModel();
208                 return true;
209         }
210         else
211         {
212                 return false;
213         }
214 }
215
216 const AudioFileModel &FileListModel::getFile(const QModelIndex &index)
217 {
218         if(index.row() >= 0 && index.row() < m_fileList.count())
219         {
220                 return m_fileStore[m_fileList.at(index.row())];         //return m_fileStore.value(m_fileList.at(index.row()));
221         }
222         else
223         {
224                 return m_nullAudioFile;
225         }
226 }
227
228 AudioFileModel &FileListModel::operator[] (const QModelIndex &index)
229 {
230         const QString key = m_fileList.at(index.row());
231         return m_fileStore[key];
232 }
233
234 bool FileListModel::setFile(const QModelIndex &index, const AudioFileModel &audioFile)
235 {
236         if(index.row() >= 0 && index.row() < m_fileList.count())
237         {
238                 const QString oldKey = m_fileList.at(index.row());
239                 const QString newKey = MAKE_KEY(audioFile.filePath());
240                 
241                 beginResetModel();
242                 m_fileList.replace(index.row(), newKey);
243                 m_fileStore.remove(oldKey);
244                 m_fileStore.insert(newKey, audioFile);
245                 endResetModel();
246                 return true;
247         }
248         else
249         {
250                 return false;
251         }
252 }
253
254 int FileListModel::exportToCsv(const QString &outFile)
255 {
256         const int nFiles = m_fileList.count();
257         
258         bool havePosition = false, haveTitle = false, haveArtist = false, haveAlbum = false, haveGenre = false, haveYear = false, haveComment = false;
259         
260         for(int i = 0; i < nFiles; i++)
261         {
262                 const AudioFileModel &current = m_fileStore.value(m_fileList.at(i));
263                 const AudioFileModel_MetaInfo &metaInfo = current.metaInfo();
264                 
265                 if(metaInfo.position() > 0) havePosition = true;
266                 if(!metaInfo.title().isEmpty()) haveTitle = true;
267                 if(!metaInfo.artist().isEmpty()) haveArtist = true;
268                 if(!metaInfo.album().isEmpty()) haveAlbum = true;
269                 if(!metaInfo.genre().isEmpty()) haveGenre = true;
270                 if(metaInfo.year() > 0) haveYear = true;
271                 if(!metaInfo.comment().isEmpty()) haveComment = true;
272         }
273
274         if(!(haveTitle || haveArtist || haveAlbum || haveGenre || haveYear || haveComment))
275         {
276                 return CsvError_NoTags;
277         }
278
279         QFile file(outFile);
280
281         if(!file.open(QIODevice::WriteOnly))
282         {
283                 return CsvError_FileOpen;
284         }
285         else
286         {
287                 QStringList line;
288                 
289                 if(havePosition) line << "POSITION";
290                 if(haveTitle) line << "TITLE";
291                 if(haveArtist) line << "ARTIST";
292                 if(haveAlbum) line << "ALBUM";
293                 if(haveGenre) line << "GENRE";
294                 if(haveYear) line << "YEAR";
295                 if(haveComment) line << "COMMENT";
296
297                 if(file.write(line.join(";").append("\r\n").toUtf8().prepend("\xef\xbb\xbf")) < 1)
298                 {
299                         file.close();
300                         return CsvError_FileWrite;
301                 }
302         }
303
304         for(int i = 0; i < nFiles; i++)
305         {
306                 QStringList line;
307                 const AudioFileModel &current = m_fileStore.value(m_fileList.at(i));
308                 const AudioFileModel_MetaInfo &metaInfo = current.metaInfo();
309                 
310                 if(havePosition) line << QString::number(metaInfo.position());
311                 if(haveTitle) line << metaInfo.title().trimmed();
312                 if(haveArtist) line << metaInfo.artist().trimmed();
313                 if(haveAlbum) line << metaInfo.album().trimmed();
314                 if(haveGenre) line << metaInfo.genre().trimmed();
315                 if(haveYear) line << QString::number(metaInfo.year());
316                 if(haveComment) line << metaInfo.comment().trimmed();
317
318                 if(file.write(line.replaceInStrings(";", ",").join(";").append("\r\n").toUtf8()) < 1)
319                 {
320                         file.close();
321                         return CsvError_FileWrite;
322                 }
323         }
324
325         file.close();
326         return CsvError_OK;
327 }
328
329 int FileListModel::importFromCsv(QWidget *parent, const QString &inFile)
330 {
331         QFile file(inFile);
332         if(!file.open(QIODevice::ReadOnly))
333         {
334                 return CsvError_FileOpen;
335         }
336
337         QTextCodec *codec = NULL;
338         QByteArray bomCheck = file.peek(16);
339
340         if((!bomCheck.isEmpty()) && bomCheck.startsWith("\xef\xbb\xbf"))
341         {
342                 codec = QTextCodec::codecForName("UTF-8");
343         }
344         else if((!bomCheck.isEmpty()) && bomCheck.startsWith("\xff\xfe"))
345         {
346                 codec = QTextCodec::codecForName("UTF-16LE");
347         }
348         else if((!bomCheck.isEmpty()) && bomCheck.startsWith("\xfe\xff"))
349         {
350                 codec = QTextCodec::codecForName("UTF-16BE");
351         }
352         else
353         {
354                 const QString systemDefault = tr("(System Default)");
355
356                 QStringList codecList;
357                 codecList.append(systemDefault);
358                 codecList.append(MUtils::available_codepages());
359
360                 QInputDialog *input = new QInputDialog(parent);
361                 input->setLabelText(EXPAND(tr("Select ANSI Codepage for CSV file:")));
362                 input->setOkButtonText(tr("OK"));
363                 input->setCancelButtonText(tr("Cancel"));
364                 input->setTextEchoMode(QLineEdit::Normal);
365                 input->setComboBoxItems(codecList);
366         
367                 if(input->exec() < 1)
368                 {
369                         MUTILS_DELETE(input);
370                         return CsvError_Aborted;
371                 }
372         
373                 if(input->textValue().compare(systemDefault, Qt::CaseInsensitive))
374                 {
375                         qDebug("User-selected codec is: %s", input->textValue().toLatin1().constData());
376                         codec = QTextCodec::codecForName(input->textValue().toLatin1().constData());
377                 }
378                 else
379                 {
380                         qDebug("Going to use the system's default codec!");
381                         codec = QTextCodec::codecForName("System");
382                 }
383
384                 MUTILS_DELETE(input);
385         }
386
387         bomCheck.clear();
388
389         //----------------------//
390
391         QTextStream stream(&file);
392         stream.setAutoDetectUnicode(false);
393         stream.setCodec(codec);
394
395         QString headerLine = stream.readLine().simplified();
396
397         while(headerLine.isEmpty())
398         {
399                 if(stream.atEnd())
400                 {
401                         qWarning("The file appears to be empty!");
402                         return CsvError_FileRead;
403                 }
404                 qWarning("Skipping a blank line at beginning of CSV file!");
405                 headerLine = stream.readLine().simplified();
406         }
407
408         QStringList header = headerLine.split(";", QString::KeepEmptyParts);
409
410         const int nCols = header.count();
411         const int nFiles = m_fileList.count();
412
413         if(nCols < 1)
414         {
415                 qWarning("Header appears to be empty!");
416                 return CsvError_FileRead;
417         }
418
419         bool *ignore = new bool[nCols];
420         memset(ignore, 0, sizeof(bool) * nCols);
421
422         for(int i = 0; i < nCols; i++)
423         {
424                 if((header[i] = header[i].trimmed()).isEmpty())
425                 {
426                         ignore[i] = true;
427                 }
428         }
429
430         //----------------------//
431
432         for(int i = 0; i < nFiles; i++)
433         {
434                 if(stream.atEnd())
435                 {
436                         MUTILS_DELETE_ARRAY(ignore);
437                         return CsvError_Incomplete;
438                 }
439                 
440                 QString line = stream.readLine().simplified();
441                 
442                 if(line.isEmpty())
443                 {
444                         qWarning("Skipping a blank line in CSV file!");
445                         continue;
446                 }
447                 
448                 QStringList data = line.split(";", QString::KeepEmptyParts);
449
450                 if(data.count() < header.count())
451                 {
452                         qWarning("Skipping an incomplete line in CSV file!");
453                         continue;
454                 }
455
456                 const QString key = m_fileList[i];
457
458                 for(int j = 0; j < nCols; j++)
459                 {
460                         if(ignore[j])
461                         {
462                                 continue;
463                         }
464                         else if(CHECK_HDR(header.at(j), "POSITION"))
465                         {
466                                 bool ok = false;
467                                 unsigned int temp = data.at(j).trimmed().toUInt(&ok);
468                                 if(ok) m_fileStore[key].metaInfo().setPosition(temp);
469                         }
470                         else if(CHECK_HDR(header.at(j), "TITLE"))
471                         {
472                                 QString temp = data.at(j).trimmed();
473                                 if(!temp.isEmpty()) m_fileStore[key].metaInfo().setTitle(temp);
474                         }
475                         else if(CHECK_HDR(header.at(j), "ARTIST"))
476                         {
477                                 QString temp = data.at(j).trimmed();
478                                 if(!temp.isEmpty()) m_fileStore[key].metaInfo().setArtist(temp);
479                         }
480                         else if(CHECK_HDR(header.at(j), "ALBUM"))
481                         {
482                                 QString temp = data.at(j).trimmed();
483                                 if(!temp.isEmpty()) m_fileStore[key].metaInfo().setAlbum(temp);
484                         }
485                         else if(CHECK_HDR(header.at(j), "GENRE"))
486                         {
487                                 QString temp = data.at(j).trimmed();
488                                 if(!temp.isEmpty()) m_fileStore[key].metaInfo().setGenre(temp);
489                         }
490                         else if(CHECK_HDR(header.at(j), "YEAR"))
491                         {
492                                 bool ok = false;
493                                 unsigned int temp = data.at(j).trimmed().toUInt(&ok);
494                                 if(ok) m_fileStore[key].metaInfo().setYear(temp);
495                         }
496                         else if(CHECK_HDR(header.at(j), "COMMENT"))
497                         {
498                                 QString temp = data.at(j).trimmed();
499                                 if(!temp.isEmpty()) m_fileStore[key].metaInfo().setComment(temp);
500                         }
501                         else
502                         {
503                                 qWarning("Unkonw field '%s' will be ignored!", MUTILS_UTF8(header.at(j)));
504                                 ignore[j] = true;
505                                 
506                                 if(!checkArray(ignore, false, nCols))
507                                 {
508                                         qWarning("No known fields left, aborting!");
509                                         return CsvError_NoTags;
510                                 }
511                         }
512                 }
513         }
514
515         //----------------------//
516
517         MUTILS_DELETE_ARRAY(ignore);
518         return CsvError_OK;
519 }
520
521 bool FileListModel::checkArray(const bool *a, const bool val, size_t len)
522 {
523         for(size_t i = 0; i < len; i++)
524         {
525                 if(a[i] == val) return true;
526         }
527
528         return false;
529 }