]> git.sesse.net Git - kdenlive/blob - src/documentchecker.cpp
Fix detection of missing luma files on document opening
[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 "docclipbase.h"
24 #include "titlewidget.h"
25 #include "definitions.h"
26 #include "kdenlivesettings.h"
27
28 #include <KDebug>
29 #include <KGlobalSettings>
30 #include <KFileItem>
31 #include <KIO/NetAccess>
32 #include <KFileDialog>
33 #include <KApplication>
34 #include <KUrlRequesterDialog>
35 #include <KMessageBox>
36
37 #include <QTreeWidgetItem>
38 #include <QFile>
39 #include <QHeaderView>
40 #include <QIcon>
41 #include <QPixmap>
42 #include <QTimer>
43 #include <QCryptographicHash>
44
45 const int hashRole = Qt::UserRole;
46 const int sizeRole = Qt::UserRole + 1;
47 const int idRole = Qt::UserRole + 2;
48 const int statusRole = Qt::UserRole + 3;
49 const int typeRole = Qt::UserRole + 4;
50 const int typeOriginalResource = Qt::UserRole + 5;
51
52 const int CLIPMISSING = 0;
53 const int CLIPOK = 1;
54 const int CLIPPLACEHOLDER = 2;
55 const int LUMAMISSING = 10;
56 const int LUMAOK = 11;
57 const int LUMAPLACEHOLDER = 12;
58
59 enum TITLECLIPTYPE { TITLE_IMAGE_ELEMENT = 20, TITLE_FONT_ELEMENT = 21 };
60
61 DocumentChecker::DocumentChecker(QDomNodeList infoproducers, QDomDocument doc):
62         m_info(infoproducers), m_doc(doc), m_dialog(NULL)
63 {
64
65 }
66
67
68 bool DocumentChecker::hasMissingClips()
69 {
70     int clipType;
71     QDomElement e;
72     QString id;
73     QString resource;
74     QList <QDomElement> missingClips;
75     for (int i = 0; i < m_info.count(); i++) {
76         e = m_info.item(i).toElement();
77         clipType = e.attribute("type").toInt();
78         if (clipType == COLOR) continue;
79         if (clipType == TEXT) {
80             //TODO: Check is clip template is missing (xmltemplate) or hash changed
81             QStringList images = TitleWidget::extractImageList(e.attribute("xmldata"));
82             QStringList fonts = TitleWidget::extractFontList(e.attribute("xmldata"));
83             checkMissingImages(missingClips, images, fonts, e.attribute("id"), e.attribute("name"));
84             continue;
85         }
86         id = e.attribute("id");
87         resource = e.attribute("resource");
88         if (clipType == SLIDESHOW) resource = KUrl(resource).directory();
89         if (!KIO::NetAccess::exists(KUrl(resource), KIO::NetAccess::SourceSide, 0)) {
90             // Missing clip found
91             missingClips.append(e);
92         } else {
93             // Check if the clip has changed
94             if (clipType != SLIDESHOW && e.hasAttribute("file_hash")) {
95                 if (e.attribute("file_hash") != DocClipBase::getHash(e.attribute("resource")))
96                     e.removeAttribute("file_hash");
97             }
98         }
99     }
100
101     QStringList missingLumas;
102     QString root = m_doc.documentElement().attribute("root");
103     if (!root.isEmpty()) root = KUrl(root).path(KUrl::AddTrailingSlash);
104     QDomNodeList trans = m_doc.elementsByTagName("transition");
105     for (int i = 0; i < trans.count(); i++) {
106         QString luma = getProperty(trans.at(i).toElement(), "luma");
107         if (!luma.startsWith('/')) luma.prepend(root);
108         if (!luma.isEmpty() && !QFile::exists(luma)) {
109             if (!missingLumas.contains(luma)) {
110                 missingLumas.append(luma);
111             }
112         }
113     }
114
115     if (missingClips.isEmpty() && missingLumas.isEmpty()) {
116         return false;
117     }
118     m_dialog = new QDialog();
119     m_dialog->setFont(KGlobalSettings::toolBarFont());
120     m_ui.setupUi(m_dialog);
121
122     foreach(const QString l, missingLumas) {
123         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Luma file") << l);
124         item->setIcon(0, KIcon("dialog-close"));
125         item->setData(0, idRole, l);
126         item->setData(0, statusRole, LUMAMISSING);
127     }
128
129     m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
130     for (int i = 0; i < missingClips.count(); i++) {
131         e = missingClips.at(i).toElement();
132         QString clipType;
133         int t = e.attribute("type").toInt();
134         switch (t) {
135         case AV:
136             clipType = i18n("Video clip");
137             break;
138         case VIDEO:
139             clipType = i18n("Mute video clip");
140             break;
141         case AUDIO:
142             clipType = i18n("Audio clip");
143             break;
144         case PLAYLIST:
145             clipType = i18n("Playlist clip");
146             break;
147         case IMAGE:
148             clipType = i18n("Image clip");
149             break;
150         case SLIDESHOW:
151             clipType = i18n("Slideshow clip");
152             break;
153         case TITLE_IMAGE_ELEMENT:
154             clipType = i18n("Title Image");
155             break;
156         case TITLE_FONT_ELEMENT:
157             clipType = i18n("Title Font");
158             break;
159         default:
160             clipType = i18n("Video clip");
161         }
162         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType);
163         if (t == TITLE_IMAGE_ELEMENT) {
164             item->setIcon(0, KIcon("dialog-warning"));
165             item->setToolTip(1, e.attribute("name"));
166             item->setText(1, e.attribute("resource"));
167             item->setData(0, statusRole, CLIPPLACEHOLDER);
168             item->setData(0, typeOriginalResource, e.attribute("resource"));
169         } else if (t == TITLE_FONT_ELEMENT) {
170             item->setIcon(0, KIcon("dialog-warning"));
171             item->setToolTip(1, e.attribute("name"));
172             QString ft = e.attribute("resource");
173             QString newft = QFontInfo(QFont(ft)).family();
174             item->setText(1, i18n("%1 will be replaced by %2", ft, newft));
175             item->setData(0, statusRole, CLIPPLACEHOLDER);
176         } else {
177             item->setIcon(0, KIcon("dialog-close"));
178             item->setText(1, e.attribute("resource"));
179             item->setData(0, hashRole, e.attribute("file_hash"));
180             item->setData(0, sizeRole, e.attribute("file_size"));
181             item->setData(0, statusRole, CLIPMISSING);
182         }
183         item->setData(0, typeRole, t);
184         item->setData(0, idRole, e.attribute("id"));
185     }
186     connect(m_ui.recursiveSearch, SIGNAL(pressed()), this, SLOT(slotSearchClips()));
187     connect(m_ui.usePlaceholders, SIGNAL(pressed()), this, SLOT(slotPlaceholders()));
188     connect(m_ui.removeSelected, SIGNAL(pressed()), this, SLOT(slotDeleteSelected()));
189     connect(m_ui.treeWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem *, int)), this, SLOT(slotEditItem(QTreeWidgetItem *, int)));
190     connect(m_ui.treeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(slotCheckButtons()));
191     //adjustSize();
192     if (m_ui.treeWidget->topLevelItem(0)) m_ui.treeWidget->setCurrentItem(m_ui.treeWidget->topLevelItem(0));
193     checkStatus();
194     int acceptMissing = m_dialog->exec();
195     if (acceptMissing == QDialog::Accepted) acceptDialog();
196     return (acceptMissing != QDialog::Accepted);
197 }
198
199 DocumentChecker::~DocumentChecker()
200 {
201     if (m_dialog) delete m_dialog;
202 }
203
204
205 QString DocumentChecker::getProperty(QDomElement effect, const QString &name)
206 {
207     QDomNodeList params = effect.elementsByTagName("property");
208     for (int i = 0; i < params.count(); i++) {
209         QDomElement e = params.item(i).toElement();
210         if (e.attribute("name") == name) {
211             return e.firstChild().nodeValue();
212         }
213     }
214     return QString();
215 }
216
217 void DocumentChecker::setProperty(QDomElement effect, const QString &name, const QString value)
218 {
219     QDomNodeList params = effect.elementsByTagName("property");
220     for (int i = 0; i < params.count(); i++) {
221         QDomElement e = params.item(i).toElement();
222         if (e.attribute("name") == name) {
223             e.firstChild().setNodeValue(value);
224         }
225     }
226 }
227
228 void DocumentChecker::slotSearchClips()
229 {
230     QString newpath = KFileDialog::getExistingDirectory(KUrl("kfiledialog:///clipfolder"), kapp->activeWindow(), i18n("Clips folder"));
231     if (newpath.isEmpty()) return;
232     int ix = 0;
233     m_ui.recursiveSearch->setEnabled(false);
234     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
235     while (child) {
236         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
237             QString clipPath = searchFileRecursively(QDir(newpath), child->data(0, sizeRole).toString(), child->data(0, hashRole).toString());
238             if (!clipPath.isEmpty()) {
239                 child->setText(1, clipPath);
240                 child->setIcon(0, KIcon("dialog-ok"));
241                 child->setData(0, statusRole, CLIPOK);
242             }
243         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
244             QString fileName = searchLuma(child->data(0, idRole).toString());
245             if (!fileName.isEmpty()) {
246                 child->setText(1, fileName);
247                 child->setIcon(0, KIcon("dialog-ok"));
248                 child->setData(0, statusRole, LUMAOK);
249             }
250         }
251         ix++;
252         child = m_ui.treeWidget->topLevelItem(ix);
253     }
254     m_ui.recursiveSearch->setEnabled(true);
255     checkStatus();
256 }
257
258
259 QString DocumentChecker::searchLuma(QString file) const
260 {
261     KUrl searchPath(KdenliveSettings::mltpath());
262     if (file.contains("PAL"))
263         searchPath.cd("../lumas/PAL");
264     else
265         searchPath.cd("../lumas/NTSC");
266     QString result = searchPath.path(KUrl::AddTrailingSlash) + KUrl(file).fileName();
267     if (QFile::exists(result))
268         return result;
269     return QString();
270 }
271
272
273 QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const
274 {
275     QString foundFileName;
276     QByteArray fileData;
277     QByteArray fileHash;
278     QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable);
279     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
280         QFile file(dir.absoluteFilePath(filesAndDirs.at(i)));
281         if (QString::number(file.size()) == matchSize) {
282             if (file.open(QIODevice::ReadOnly)) {
283                 /*
284                 * 1 MB = 1 second per 450 files (or faster)
285                 * 10 MB = 9 seconds per 450 files (or faster)
286                 */
287                 if (file.size() > 1000000*2) {
288                     fileData = file.read(1000000);
289                     if (file.seek(file.size() - 1000000))
290                         fileData.append(file.readAll());
291                 } else
292                     fileData = file.readAll();
293                 file.close();
294                 fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
295                 if (QString(fileHash.toHex()) == matchHash)
296                     return file.fileName();
297             }
298         }
299         //kDebug() << filesAndDirs.at(i) << file.size() << fileHash.toHex();
300     }
301     filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
302     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
303         foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash);
304         if (!foundFileName.isEmpty())
305             break;
306     }
307     return foundFileName;
308 }
309
310 void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int)
311 {
312     int t = item->data(0, typeRole).toInt();
313     if (t == TITLE_FONT_ELEMENT) return;
314     //|| t == TITLE_IMAGE_ELEMENT) {
315
316     KUrl url = KUrlRequesterDialog::getUrl(item->text(1), m_dialog, i18n("Enter new location for file"));
317     if (url.isEmpty()) return;
318     item->setText(1, url.path());
319     if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0)) {
320         item->setIcon(0, KIcon("dialog-ok"));
321         int id = item->data(0, statusRole).toInt();
322         if (id < 10) item->setData(0, statusRole, CLIPOK);
323         else item->setData(0, statusRole, LUMAOK);
324         checkStatus();
325     } else {
326         item->setIcon(0, KIcon("dialog-close"));
327         int id = item->data(0, statusRole).toInt();
328         if (id < 10) item->setData(0, statusRole, CLIPMISSING);
329         else item->setData(0, statusRole, LUMAMISSING);
330         checkStatus();
331     }
332 }
333
334
335 void DocumentChecker::acceptDialog()
336 {
337     QDomElement e, property;
338     QDomNodeList producers = m_doc.elementsByTagName("producer");
339     QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
340     QDomNodeList properties;
341     int ix = 0;
342
343     // prepare transitions
344     QDomNodeList trans = m_doc.elementsByTagName("transition");
345
346     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
347     while (child) {
348         int t = child->data(0, typeRole).toInt();
349         if (child->data(0, statusRole).toInt() == CLIPOK) {
350             QString id = child->data(0, idRole).toString();
351             if (t == TITLE_IMAGE_ELEMENT) {
352                 // edit images embedded in titles
353                 for (int i = 0; i < infoproducers.count(); i++) {
354                     e = infoproducers.item(i).toElement();
355                     if (e.attribute("id") == id) {
356                         // Fix clip
357                         QString xml = e.attribute("xmldata");
358                         xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
359                         e.setAttribute("xmldata", xml);
360                         break;
361                     }
362                 }
363                 for (int i = 0; i < producers.count(); i++) {
364                     e = producers.item(i).toElement();
365                     if (e.attribute("id").section('_', 0, 0) == id) {
366                         // Fix clip
367                         properties = e.childNodes();
368                         for (int j = 0; j < properties.count(); ++j) {
369                             property = properties.item(j).toElement();
370                             if (property.attribute("name") == "xmldata") {
371                                 QString xml = property.firstChild().nodeValue();
372                                 xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
373                                 property.firstChild().setNodeValue(xml);
374                                 break;
375                             }
376                         }
377                     }
378                 }
379             } else {
380                 // edit clip url
381                 for (int i = 0; i < infoproducers.count(); i++) {
382                     e = infoproducers.item(i).toElement();
383                     if (e.attribute("id") == id) {
384                         // Fix clip
385                         e.setAttribute("resource", child->text(1));
386                         e.setAttribute("name", KUrl(child->text(1)).fileName());
387                         break;
388                     }
389                 }
390                 for (int i = 0; i < producers.count(); i++) {
391                     e = producers.item(i).toElement();
392                     if (e.attribute("id").section('_', 0, 0) == id) {
393                         // Fix clip
394                         properties = e.childNodes();
395                         for (int j = 0; j < properties.count(); ++j) {
396                             property = properties.item(j).toElement();
397                             if (property.attribute("name") == "resource") {
398                                 property.firstChild().setNodeValue(child->text(1));
399                                 break;
400                             }
401                         }
402                     }
403                 }
404             }
405         } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER && t != TITLE_FONT_ELEMENT && t != TITLE_IMAGE_ELEMENT) {
406             QString id = child->data(0, idRole).toString();
407             for (int i = 0; i < infoproducers.count(); i++) {
408                 e = infoproducers.item(i).toElement();
409                 if (e.attribute("id") == id) {
410                     // Fix clip
411                     e.setAttribute("placeholder", '1');
412                     break;
413                 }
414             }
415         } else if (child->data(0, statusRole).toInt() == LUMAOK) {
416             for (int i = 0; i < trans.count(); i++) {
417                 QString luma = getProperty(trans.at(i).toElement(), "luma");
418                 kDebug() << "luma: " << luma;
419                 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
420                     setProperty(trans.at(i).toElement(), "luma", child->text(1));
421                     kDebug() << "replace with; " << child->text(1);
422                 }
423             }
424         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
425             for (int i = 0; i < trans.count(); i++) {
426                 QString luma = getProperty(trans.at(i).toElement(), "luma");
427                 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
428                     setProperty(trans.at(i).toElement(), "luma", QString());
429                 }
430             }
431         }
432         ix++;
433         child = m_ui.treeWidget->topLevelItem(ix);
434     }
435     //QDialog::accept();
436 }
437
438 void DocumentChecker::slotPlaceholders()
439 {
440     int ix = 0;
441     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
442     while (child) {
443         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
444             child->setData(0, statusRole, CLIPPLACEHOLDER);
445             child->setIcon(0, KIcon("dialog-ok"));
446         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
447             child->setData(0, statusRole, LUMAPLACEHOLDER);
448             child->setIcon(0, KIcon("dialog-ok"));
449         }
450         ix++;
451         child = m_ui.treeWidget->topLevelItem(ix);
452     }
453     checkStatus();
454 }
455
456
457 void DocumentChecker::checkStatus()
458 {
459     bool status = true;
460     int ix = 0;
461     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
462     while (child) {
463         if (child->data(0, statusRole).toInt() == CLIPMISSING || child->data(0, statusRole).toInt() == LUMAMISSING) {
464             status = false;
465             break;
466         }
467         ix++;
468         child = m_ui.treeWidget->topLevelItem(ix);
469     }
470     m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status);
471 }
472
473
474 void DocumentChecker::slotDeleteSelected()
475 {
476     if (KMessageBox::warningContinueCancel(m_dialog, i18np("This will remove the selected clip from this project", "This will remove the selected clips from this project", m_ui.treeWidget->selectedItems().count()), i18n("Remove clips")) == KMessageBox::Cancel) return;
477     int ix = 0;
478     QStringList deletedIds;
479     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
480     QDomNodeList playlists = m_doc.elementsByTagName("playlist");
481
482     while (child) {
483         int id = child->data(0, statusRole).toInt();
484         if (child->isSelected() && id < 10) {
485             QString id = child->data(0, idRole).toString();
486             deletedIds.append(id);
487             for (int j = 0; j < playlists.count(); j++)
488                 deletedIds.append(id + '_' + QString::number(j));
489             delete child;
490         } else ix++;
491         child = m_ui.treeWidget->topLevelItem(ix);
492     }
493     kDebug() << "// Clips to delete: " << deletedIds;
494
495     if (!deletedIds.isEmpty()) {
496         QDomElement e;
497         QDomNodeList producers = m_doc.elementsByTagName("producer");
498         QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
499
500         QDomElement mlt = m_doc.firstChildElement("mlt");
501         QDomElement kdenlivedoc = mlt.firstChildElement("kdenlivedoc");
502
503         for (int i = 0; i < infoproducers.count(); i++) {
504             e = infoproducers.item(i).toElement();
505             if (deletedIds.contains(e.attribute("id"))) {
506                 // Remove clip
507                 kdenlivedoc.removeChild(e);
508                 break;
509             }
510         }
511
512         for (int i = 0; i < producers.count(); i++) {
513             e = producers.item(i).toElement();
514             if (deletedIds.contains(e.attribute("id"))) {
515                 // Remove clip
516                 mlt.removeChild(e);
517                 break;
518             }
519         }
520
521         for (int i = 0; i < playlists.count(); i++) {
522             QDomNodeList entries = playlists.at(i).toElement().elementsByTagName("entry");
523             for (int j = 0; j < playlists.count(); j++) {
524                 e = entries.item(j).toElement();
525                 if (deletedIds.contains(e.attribute("producer"))) {
526                     // Replace clip with blank
527                     e.setTagName("blank");
528                     e.removeAttribute("producer");
529                     int length = e.attribute("out").toInt() - e.attribute("in").toInt();
530                     e.setAttribute("length", length);
531                 }
532             }
533         }
534         checkStatus();
535     }
536 }
537
538 void DocumentChecker::checkMissingImages(QList <QDomElement>&missingClips, QStringList images, QStringList fonts, QString id, QString baseClip)
539 {
540     QDomDocument doc;
541     foreach(const QString &img, images) {
542         if (!KIO::NetAccess::exists(KUrl(img), KIO::NetAccess::SourceSide, 0)) {
543             QDomElement e = doc.createElement("missingclip");
544             e.setAttribute("type", TITLE_IMAGE_ELEMENT);
545             e.setAttribute("resource", img);
546             e.setAttribute("id", id);
547             e.setAttribute("name", baseClip);
548             missingClips.append(e);
549         }
550     }
551     kDebug() << "/ / / CHK FONTS: " << fonts;
552     foreach(const QString &fontelement, fonts) {
553         QFont f(fontelement);
554         kDebug() << "/ / / CHK FONTS: " << fontelement << " = " << QFontInfo(f).family();
555         if (fontelement != QFontInfo(f).family()) {
556             QDomElement e = doc.createElement("missingclip");
557             e.setAttribute("type", TITLE_FONT_ELEMENT);
558             e.setAttribute("resource", fontelement);
559             e.setAttribute("id", id);
560             e.setAttribute("name", baseClip);
561             missingClips.append(e);
562         }
563     }
564 }
565
566
567 void DocumentChecker::slotCheckButtons()
568 {
569     if (m_ui.treeWidget->currentItem()) {
570         QTreeWidgetItem *item = m_ui.treeWidget->currentItem();
571         int t = item->data(0, typeRole).toInt();
572         if (t == TITLE_FONT_ELEMENT || t == TITLE_IMAGE_ELEMENT) {
573             m_ui.removeSelected->setEnabled(false);
574         } else m_ui.removeSelected->setEnabled(true);
575     }
576
577 }
578
579 #include "documentchecker.moc"
580
581