]> git.sesse.net Git - kdenlive/blob - src/documentchecker.cpp
84bf683206768b295c860134883d7791996ecc11
[kdenlive] / src / documentchecker.cpp
1 /***************************************************************************
2  *   Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org)        *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, write to the                         *
16  *   Free Software Foundation, Inc.,                                       *
17  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA          *
18  ***************************************************************************/
19
20
21 #include "documentchecker.h"
22 #include "kthumb.h"
23 #include "definitions.h"
24 #include "kdenlivesettings.h"
25
26 #include <KDebug>
27 #include <KGlobalSettings>
28 #include <KFileItem>
29 #include <KIO/NetAccess>
30 #include <KFileDialog>
31 #include <KApplication>
32 #include <KUrlRequesterDialog>
33 #include <KMessageBox>
34
35 #include <QTreeWidgetItem>
36 #include <QFile>
37 #include <QHeaderView>
38 #include <QIcon>
39 #include <QPixmap>
40 #include <QTimer>
41 #include <QCryptographicHash>
42
43 const int hashRole = Qt::UserRole;
44 const int sizeRole = Qt::UserRole + 1;
45 const int idRole = Qt::UserRole + 2;
46 const int statusRole = Qt::UserRole + 3;
47
48 const int CLIPMISSING = 0;
49 const int CLIPOK = 1;
50 const int CLIPPLACEHOLDER = 2;
51 const int LUMAMISSING = 10;
52 const int LUMAOK = 11;
53 const int LUMAPLACEHOLDER = 12;
54
55 DocumentChecker::DocumentChecker(QList <QDomElement> missingClips, QDomDocument doc, QWidget * parent) :
56         QDialog(parent),
57         m_doc(doc)
58 {
59     setFont(KGlobalSettings::toolBarFont());
60     setupUi(this);
61     QDomElement e;
62     QStringList missingLumas;
63     QDomNodeList trans = doc.elementsByTagName("transition");
64     for (int i = 0; i < trans.count(); i++) {
65         QString luma = getProperty(trans.at(i).toElement(), "luma");
66         if (!luma.isEmpty() && !QFile::exists(luma)) {
67             if (!missingLumas.contains(luma)) {
68                 missingLumas.append(luma);
69                 QTreeWidgetItem *item = new QTreeWidgetItem(treeWidget, QStringList() << i18n("Luma file") << luma);
70                 item->setIcon(0, KIcon("dialog-close"));
71                 item->setData(0, idRole, luma);
72                 item->setData(0, statusRole, LUMAMISSING);
73             }
74         }
75     }
76
77     buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
78     for (int i = 0; i < missingClips.count(); i++) {
79         e = missingClips.at(i).toElement();
80         QString clipType;
81         switch (e.attribute("type").toInt()) {
82         case AV:
83             clipType = i18n("Video clip");
84             break;
85         case VIDEO:
86             clipType = i18n("Mute video clip");
87             break;
88         case AUDIO:
89             clipType = i18n("Audio clip");
90             break;
91         case PLAYLIST:
92             clipType = i18n("Playlist clip");
93             break;
94         case IMAGE:
95             clipType = i18n("Image clip");
96             break;
97         case SLIDESHOW:
98             clipType = i18n("Slideshow clip");
99             break;
100         default:
101             clipType = i18n("Video clip");
102         }
103         QTreeWidgetItem *item = new QTreeWidgetItem(treeWidget, QStringList() << clipType << e.attribute("resource"));
104         item->setIcon(0, KIcon("dialog-close"));
105         item->setData(0, hashRole, e.attribute("file_hash"));
106         item->setData(0, sizeRole, e.attribute("file_size"));
107         item->setData(0, idRole, e.attribute("id"));
108         item->setData(0, statusRole, CLIPMISSING);
109     }
110     connect(recursiveSearch, SIGNAL(pressed()), this, SLOT(slotSearchClips()));
111     connect(usePlaceholders, SIGNAL(pressed()), this, SLOT(slotPlaceholders()));
112     connect(removeSelected, SIGNAL(pressed()), this, SLOT(slotDeleteSelected()));
113     connect(treeWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem *, int)), this, SLOT(slotEditItem(QTreeWidgetItem *, int)));
114     //adjustSize();
115 }
116
117 DocumentChecker::~DocumentChecker() {}
118
119
120 QString DocumentChecker::getProperty(QDomElement effect, const QString &name)
121 {
122     QDomNodeList params = effect.elementsByTagName("property");
123     for (int i = 0; i < params.count(); i++) {
124         QDomElement e = params.item(i).toElement();
125         if (e.attribute("name") == name) {
126             return e.firstChild().nodeValue();
127         }
128     }
129     return QString();
130 }
131
132 void DocumentChecker::setProperty(QDomElement effect, const QString &name, const QString value)
133 {
134     QDomNodeList params = effect.elementsByTagName("property");
135     for (int i = 0; i < params.count(); i++) {
136         QDomElement e = params.item(i).toElement();
137         if (e.attribute("name") == name) {
138             e.firstChild().setNodeValue(value);
139         }
140     }
141 }
142
143 void DocumentChecker::slotSearchClips()
144 {
145     QString newpath = KFileDialog::getExistingDirectory(KUrl("kfiledialog:///clipfolder"), kapp->activeWindow(), i18n("Clips folder"));
146     if (newpath.isEmpty()) return;
147     int ix = 0;
148     recursiveSearch->setEnabled(false);
149     QTreeWidgetItem *child = treeWidget->topLevelItem(ix);
150     while (child) {
151         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
152             QString clipPath = searchFileRecursively(QDir(newpath), child->data(0, sizeRole).toString(), child->data(0, hashRole).toString());
153             if (!clipPath.isEmpty()) {
154                 child->setText(1, clipPath);
155                 child->setIcon(0, KIcon("dialog-ok"));
156                 child->setData(0, statusRole, CLIPOK);
157             }
158         }
159         else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
160             QString fileName = searchLuma(child->data(0, idRole).toString());
161             if (!fileName.isEmpty()) {
162                 child->setText(1, fileName);
163                 child->setIcon(0, KIcon("dialog-ok"));
164                 child->setData(0, statusRole, LUMAOK);
165             }
166         }
167         ix++;
168         child = treeWidget->topLevelItem(ix);
169     }
170     recursiveSearch->setEnabled(true);
171     checkStatus();
172 }
173
174
175 QString DocumentChecker::searchLuma(QString file) const
176 {
177     KUrl searchPath(KdenliveSettings::mltpath());
178     if (file.contains("PAL"))
179         searchPath.cd("../lumas/PAL");
180     else
181         searchPath.cd("../lumas/NTSC");
182     QString result = searchPath.path(KUrl::AddTrailingSlash) + KUrl(file).fileName();
183     if (QFile::exists(result))
184         return result;
185     return QString();
186 }
187
188
189 QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const
190 {
191     QString foundFileName;
192     QByteArray fileData;
193     QByteArray fileHash;
194     QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable);
195     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
196         QFile file(dir.absoluteFilePath(filesAndDirs.at(i)));
197         if (QString::number(file.size()) == matchSize) {
198             if (file.open(QIODevice::ReadOnly)) {
199                 /*
200                 * 1 MB = 1 second per 450 files (or faster)
201                 * 10 MB = 9 seconds per 450 files (or faster)
202                 */
203                 if (file.size() > 1000000*2) {
204                     fileData = file.read(1000000);
205                     if (file.seek(file.size() - 1000000))
206                         fileData.append(file.readAll());
207                 } else
208                     fileData = file.readAll();
209                 file.close();
210                 fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
211                 if (QString(fileHash.toHex()) == matchHash)
212                     return file.fileName();
213             }
214         }
215         //kDebug() << filesAndDirs.at(i) << file.size() << fileHash.toHex();
216     }
217     filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
218     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
219         foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash);
220         if (!foundFileName.isEmpty())
221             break;
222     }
223     return foundFileName;
224 }
225
226 void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int)
227 {
228     KUrl url = KUrlRequesterDialog::getUrl(item->text(1), this, i18n("Enter new location for file"));
229     if (url.isEmpty()) return;
230     item->setText(1, url.path());
231     if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0)) {
232         item->setIcon(0, KIcon("dialog-ok"));
233         int id = item->data(0, statusRole).toInt();
234         if (id < 10) item->setData(0, statusRole, CLIPOK);
235         else item->setData(0, statusRole, LUMAOK);
236         checkStatus();
237     }
238 }
239
240 // virtual
241 void DocumentChecker::accept()
242 {
243     QDomElement e, property;
244     QDomNodeList producers = m_doc.elementsByTagName("producer");
245     QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
246     QDomNodeList properties;
247     int ix = 0;
248
249     // prepare transitions
250     QDomNodeList trans = m_doc.elementsByTagName("transition");
251     
252     QTreeWidgetItem *child = treeWidget->topLevelItem(ix);
253     while (child) {
254         if (child->data(0, statusRole).toInt() == CLIPOK) {
255             QString id = child->data(0, idRole).toString();
256             for (int i = 0; i < infoproducers.count(); i++) {
257                 e = infoproducers.item(i).toElement();
258                 if (e.attribute("id") == id) {
259                     // Fix clip
260                     e.setAttribute("resource", child->text(1));
261                     e.setAttribute("name", KUrl(child->text(1)).fileName());
262                     break;
263                 }
264             }
265             for (int i = 0; i < producers.count(); i++) {
266                 e = producers.item(i).toElement();
267                 if (e.attribute("id").section('_', 0, 0) == id) {
268                     // Fix clip
269                     properties = e.childNodes();
270                     for (int j = 0; j < properties.count(); ++j) {
271                         property = properties.item(j).toElement();
272                         if (property.attribute("name") == "resource") {
273                             property.firstChild().setNodeValue(child->text(1));
274                             break;
275                         }
276                     }
277                     break;
278                 }
279             }
280         } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER) {
281             QString id = child->data(0, idRole).toString();
282             for (int i = 0; i < infoproducers.count(); i++) {
283                 e = infoproducers.item(i).toElement();
284                 if (e.attribute("id") == id) {
285                     // Fix clip
286                     e.setAttribute("placeholder", '1');
287                     break;
288                 }
289             }
290         } else if (child->data(0, statusRole).toInt() == LUMAOK) {
291             for (int i = 0; i < trans.count(); i++) {
292                 QString luma = getProperty(trans.at(i).toElement(), "luma");
293                 kDebug()<< "luma: "<<luma;
294                 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
295                     setProperty(trans.at(i).toElement(), "luma", child->text(1));
296                     kDebug()<< "replace with; "<<child->text(1);
297                 }
298             }
299         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
300             for (int i = 0; i < trans.count(); i++) {
301                 QString luma = getProperty(trans.at(i).toElement(), "luma");
302                 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
303                     setProperty(trans.at(i).toElement(), "luma", QString());
304                 }
305             }
306         }
307         ix++;
308         child = treeWidget->topLevelItem(ix);
309     }
310     QDialog::accept();
311 }
312
313 void DocumentChecker::slotPlaceholders()
314 {
315     int ix = 0;
316     QTreeWidgetItem *child = treeWidget->topLevelItem(ix);
317     while (child) {
318         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
319             child->setData(0, statusRole, CLIPPLACEHOLDER);
320             child->setIcon(0, KIcon("dialog-ok"));
321         }
322         else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
323             child->setData(0, statusRole, LUMAPLACEHOLDER);
324             child->setIcon(0, KIcon("dialog-ok"));
325         }
326         ix++;
327         child = treeWidget->topLevelItem(ix);
328     }
329     checkStatus();
330 }
331
332
333 void DocumentChecker::checkStatus()
334 {
335     bool status = true;
336     int ix = 0;
337     QTreeWidgetItem *child = treeWidget->topLevelItem(ix);
338     while (child) {
339         if (child->data(0, statusRole).toInt() == CLIPMISSING || child->data(0, statusRole).toInt() == LUMAMISSING) {
340             status = false;
341             break;
342         }
343         ix++;
344         child = treeWidget->topLevelItem(ix);
345     }
346     buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status);
347 }
348
349
350 void DocumentChecker::slotDeleteSelected()
351 {
352     if (KMessageBox::warningContinueCancel(this, i18n("This will remove the selected clips from this project"), i18n("Remove clips")) == KMessageBox::Cancel) return;
353     int ix = 0;
354     QStringList deletedIds;
355     QTreeWidgetItem *child = treeWidget->topLevelItem(ix);
356     QDomNodeList playlists = m_doc.elementsByTagName("playlist");
357
358     while (child) {
359         int id = child->data(0, statusRole).toInt();
360         if (child->isSelected() && id < 10) {
361             QString id = child->data(0, idRole).toString();
362             deletedIds.append(id);
363             for (int j = 0; j < playlists.count(); j++)
364                 deletedIds.append(id + '_' + QString::number(j));
365             delete child;
366         } else ix++;
367         child = treeWidget->topLevelItem(ix);
368     }
369     kDebug() << "// Clips to delete: " << deletedIds;
370
371     if (!deletedIds.isEmpty()) {
372         QDomElement e;
373         QDomNodeList producers = m_doc.elementsByTagName("producer");
374         QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
375
376         QDomElement mlt = m_doc.firstChildElement("mlt");
377         QDomElement kdenlivedoc = mlt.firstChildElement("kdenlivedoc");
378
379         for (int i = 0; i < infoproducers.count(); i++) {
380             e = infoproducers.item(i).toElement();
381             if (deletedIds.contains(e.attribute("id"))) {
382                 // Remove clip
383                 kdenlivedoc.removeChild(e);
384                 break;
385             }
386         }
387
388         for (int i = 0; i < producers.count(); i++) {
389             e = producers.item(i).toElement();
390             if (deletedIds.contains(e.attribute("id"))) {
391                 // Remove clip
392                 mlt.removeChild(e);
393                 break;
394             }
395         }
396
397         for (int i = 0; i < playlists.count(); i++) {
398             QDomNodeList entries = playlists.at(i).toElement().elementsByTagName("entry");
399             for (int j = 0; j < playlists.count(); j++) {
400                 e = entries.item(j).toElement();
401                 if (deletedIds.contains(e.attribute("producer"))) {
402                     // Replace clip with blank
403                     e.setTagName("blank");
404                     e.removeAttribute("producer");
405                     int length = e.attribute("out").toInt() - e.attribute("in").toInt();
406                     e.setAttribute("length", length);
407                 }
408             }
409         }
410         checkStatus();
411     }
412 }
413
414 #include "documentchecker.moc"
415
416