]> git.sesse.net Git - kdenlive/blob - src/documentchecker.cpp
8614eeab673eba6dfb94f85c4dc095e3b22c3a23
[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 #include <KStandardDirs>
37
38 #include <QTreeWidgetItem>
39 #include <QFile>
40 #include <QHeaderView>
41 #include <QIcon>
42 #include <QPixmap>
43 #include <QTimer>
44 #include <QCryptographicHash>
45
46 const int hashRole = Qt::UserRole;
47 const int sizeRole = Qt::UserRole + 1;
48 const int idRole = Qt::UserRole + 2;
49 const int statusRole = Qt::UserRole + 3;
50 const int typeRole = Qt::UserRole + 4;
51 const int typeOriginalResource = Qt::UserRole + 5;
52 const int resetDurationRole = Qt::UserRole + 6;
53
54 const int CLIPMISSING = 0;
55 const int CLIPOK = 1;
56 const int CLIPPLACEHOLDER = 2;
57 const int CLIPWRONGDURATION = 3;
58 const int PROXYMISSING = 4;
59 const int SOURCEMISSING = 5;
60
61 const int LUMAMISSING = 10;
62 const int LUMAOK = 11;
63 const int LUMAPLACEHOLDER = 12;
64
65 enum TITLECLIPTYPE { TITLE_IMAGE_ELEMENT = 20, TITLE_FONT_ELEMENT = 21 };
66
67 DocumentChecker::DocumentChecker(QDomNodeList infoproducers, QDomDocument doc):
68     m_info(infoproducers), m_doc(doc), m_dialog(NULL)
69 {
70
71 }
72
73
74 bool DocumentChecker::hasErrorInClips()
75 {
76     int clipType;
77     QDomElement e;
78     QString resource;
79     int max;
80     QDomNodeList documentProducers = m_doc.elementsByTagName("producer");
81     QList <QDomElement> wrongDurationClips;
82     // List clips whose proxy is missing
83     QList <QDomElement> missingProxies;
84     // List clips who have a working proxy but no source clip
85     QList <QDomElement> missingSources;
86     m_safeImages.clear();
87     m_safeFonts.clear();
88     max = m_info.count();
89     for (int i = 0; i < max; i++) {
90         e = m_info.item(i).toElement();
91         clipType = e.attribute("type").toInt();
92         if (clipType == COLOR) continue;
93         if (clipType != TEXT && clipType != IMAGE && clipType != SLIDESHOW) {
94             QString id = e.attribute("id");
95             int duration = e.attribute("duration").toInt();
96             int mltDuration = -1;
97             QDomElement mltProd;
98             QString prodId;
99             // Check that the duration is in sync between Kdenlive's info and MLT's playlist
100             int prodsCount = documentProducers.count();
101             for (int j = 0; j < prodsCount; j++) {
102                 mltProd = documentProducers.at(j).toElement();
103                 prodId = mltProd.attribute("id");
104                 // Don't check slowmotion clips for now... (TODO?)
105                 if (prodId.startsWith("slowmotion")) continue;
106                 if (prodId.contains('_')) prodId = prodId.section('_', 0, 0);
107                 if (prodId != id) continue;
108                 if (mltDuration > 0 ) {
109                     // We have several MLT producers for the same clip (probably track producers)
110                     int newLength = EffectsList::property(mltProd, "length").toInt();
111                     if (newLength != mltDuration) {
112                         // we have a different duration for the same clip, that is not safe
113                         e.setAttribute("_resetDuration", 1);
114                     }
115                 }
116                 mltDuration = EffectsList::property(mltProd, "length").toInt();
117                 if (mltDuration != duration) {
118                     // Duration mismatch
119                     e.setAttribute("_mismatch", mltDuration);
120                     if (mltDuration == 15000) {
121                         // a length of 15000 might indicate a wrong clip length since it is a default length
122                         e.setAttribute("_resetDuration", 1);
123                     }
124                     if (!wrongDurationClips.contains(e)) wrongDurationClips.append(e);
125                 }
126             }
127         }
128         
129         if (clipType == TEXT) {
130             //TODO: Check is clip template is missing (xmltemplate) or hash changed
131             QStringList images = TitleWidget::extractImageList(e.attribute("xmldata"));
132             QStringList fonts = TitleWidget::extractFontList(e.attribute("xmldata"));
133             checkMissingImagesAndFonts(images, fonts, e.attribute("id"), e.attribute("name"));
134             continue;
135         }
136         resource = e.attribute("resource");
137         if (e.hasAttribute("proxy")) {
138             QString proxyresource = e.attribute("proxy");
139             if (!proxyresource.isEmpty() && proxyresource != "-") {
140                 // clip has a proxy
141                 if (!KIO::NetAccess::exists(KUrl(proxyresource), KIO::NetAccess::SourceSide, 0)) {
142                     // Missing clip found
143                     missingProxies.append(e);
144                 }
145                 else if (!KIO::NetAccess::exists(KUrl(resource), KIO::NetAccess::SourceSide, 0)) {
146                     // clip has proxy but original clip is missing
147                     missingSources.append(e);
148                     continue;
149                 }
150             }
151         }
152         if (clipType == SLIDESHOW) resource = KUrl(resource).directory();
153         if (!KIO::NetAccess::exists(KUrl(resource), KIO::NetAccess::SourceSide, 0)) {
154             // Missing clip found
155             m_missingClips.append(e);
156         } else {
157             // Check if the clip has changed
158             if (clipType != SLIDESHOW && e.hasAttribute("file_hash")) {
159                 if (e.attribute("file_hash") != DocClipBase::getHash(e.attribute("resource")))
160                     e.removeAttribute("file_hash");
161             }
162         }
163     }
164
165     // Get list of used Luma files
166     QStringList missingLumas;
167     QStringList filesToCheck;
168     QString filePath;
169     QString root = m_doc.documentElement().attribute("root");
170     if (!root.isEmpty()) root = KUrl(root).path(KUrl::AddTrailingSlash);
171     QDomNodeList trans = m_doc.elementsByTagName("transition");
172     max = trans.count();
173     for (int i = 0; i < max; i++) {
174         QString luma = getProperty(trans.at(i).toElement(), "luma");
175         if (!luma.isEmpty() && !filesToCheck.contains(luma))
176             filesToCheck.append(luma);
177     }
178     // Check existence of luma files
179     foreach (const QString lumafile, filesToCheck) {
180         filePath = lumafile;
181         if (!filePath.startsWith('/')) filePath.prepend(root);
182         if (!QFile::exists(filePath)) {
183             missingLumas.append(lumafile);
184         }
185     }
186     
187     
188
189     if (m_missingClips.isEmpty() && missingLumas.isEmpty() && wrongDurationClips.isEmpty() && missingProxies.isEmpty() && missingSources.isEmpty())
190         return false;
191
192     m_dialog = new QDialog();
193     m_dialog->setFont(KGlobalSettings::toolBarFont());
194     m_ui.setupUi(m_dialog);
195
196     foreach(const QString l, missingLumas) {
197         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Luma file") << l);
198         item->setIcon(0, KIcon("dialog-close"));
199         item->setData(0, idRole, l);
200         item->setData(0, statusRole, LUMAMISSING);
201     }
202
203     m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
204     max = m_missingClips.count();
205     for (int i = 0; i < max; i++) {
206         e = m_missingClips.at(i).toElement();
207         QString clipType;
208         int t = e.attribute("type").toInt();
209         switch (t) {
210         case AV:
211             clipType = i18n("Video clip");
212             break;
213         case VIDEO:
214             clipType = i18n("Mute video clip");
215             break;
216         case AUDIO:
217             clipType = i18n("Audio clip");
218             break;
219         case PLAYLIST:
220             clipType = i18n("Playlist clip");
221             break;
222         case IMAGE:
223             clipType = i18n("Image clip");
224             break;
225         case SLIDESHOW:
226             clipType = i18n("Slideshow clip");
227             break;
228         case TITLE_IMAGE_ELEMENT:
229             clipType = i18n("Title Image");
230             break;
231         case TITLE_FONT_ELEMENT:
232             clipType = i18n("Title Font");
233             break;
234         default:
235             clipType = i18n("Video clip");
236         }
237         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType);
238         if (t == TITLE_IMAGE_ELEMENT) {
239             item->setIcon(0, KIcon("dialog-warning"));
240             item->setToolTip(1, e.attribute("name"));
241             item->setText(1, e.attribute("resource"));
242             item->setData(0, statusRole, CLIPPLACEHOLDER);
243             item->setData(0, typeOriginalResource, e.attribute("resource"));
244         } else if (t == TITLE_FONT_ELEMENT) {
245             item->setIcon(0, KIcon("dialog-warning"));
246             item->setToolTip(1, e.attribute("name"));
247             QString ft = e.attribute("resource");
248             QString newft = QFontInfo(QFont(ft)).family();
249             item->setText(1, i18n("%1 will be replaced by %2", ft, newft));
250             item->setData(0, statusRole, CLIPPLACEHOLDER);
251         } else {
252             item->setIcon(0, KIcon("dialog-close"));
253             item->setText(1, e.attribute("resource"));
254             item->setData(0, hashRole, e.attribute("file_hash"));
255             item->setData(0, sizeRole, e.attribute("file_size"));
256             item->setData(0, statusRole, CLIPMISSING);
257         }
258         item->setData(0, typeRole, t);
259         item->setData(0, idRole, e.attribute("id"));
260         item->setToolTip(0, i18n("Missing item"));
261     }
262
263     if (m_missingClips.count() > 0) {
264         if (wrongDurationClips.count() > 0) {
265             m_ui.infoLabel->setText(i18n("The project file contains missing clips or files and clip duration mismatch"));
266         }
267         else {
268             m_ui.infoLabel->setText(i18n("The project file contains missing clips or files"));
269         }
270     }
271     else if (wrongDurationClips.count() > 0) {
272         m_ui.infoLabel->setText(i18n("The project file contains clips with duration mismatch"));
273     }
274     if (missingProxies.count() > 0) {
275         if (!m_ui.infoLabel->text().isEmpty()) m_ui.infoLabel->setText(m_ui.infoLabel->text() + ". ");
276         m_ui.infoLabel->setText(m_ui.infoLabel->text() + i18n("Missing proxies will be recreated after opening."));
277     }
278     if (missingSources.count() > 0) {
279         if (!m_ui.infoLabel->text().isEmpty()) m_ui.infoLabel->setText(m_ui.infoLabel->text() + ". ");
280         m_ui.infoLabel->setText(m_ui.infoLabel->text() + i18np("The project file contains a missing clip, you can still work with its proxy.", "The project file contains missing clips, you can still work with their proxies.", missingSources.count()));
281     }
282
283     m_ui.removeSelected->setEnabled(!m_missingClips.isEmpty());
284     m_ui.recursiveSearch->setEnabled(!m_missingClips.isEmpty() || !missingLumas.isEmpty() || !missingSources.isEmpty());
285     m_ui.usePlaceholders->setEnabled(!m_missingClips.isEmpty());
286     m_ui.fixDuration->setEnabled(!wrongDurationClips.isEmpty());
287
288     max = wrongDurationClips.count();
289     for (int i = 0; i < max; i++) {
290         e = wrongDurationClips.at(i).toElement();
291         QString clipType;
292         int t = e.attribute("type").toInt();
293         switch (t) {
294         case AV:
295             clipType = i18n("Video clip");
296             break;
297         case VIDEO:
298             clipType = i18n("Mute video clip");
299             break;
300         case AUDIO:
301             clipType = i18n("Audio clip");
302             break;
303         case PLAYLIST:
304             clipType = i18n("Playlist clip");
305             break;
306         case IMAGE:
307             clipType = i18n("Image clip");
308             break;
309         case SLIDESHOW:
310             clipType = i18n("Slideshow clip");
311             break;
312         default:
313             clipType = i18n("Video clip");
314         }
315         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType);
316         item->setIcon(0, KIcon("timeadjust"));
317         item->setText(1, e.attribute("resource"));
318         item->setData(0, hashRole, e.attribute("file_hash"));
319         item->setData(0, sizeRole, e.attribute("_mismatch"));
320         e.removeAttribute("_mismatch");
321         item->setData(0, resetDurationRole, (int) e.hasAttribute("_resetDuration"));
322         e.removeAttribute("_resetDuration");
323         item->setData(0, statusRole, CLIPWRONGDURATION);
324         item->setData(0, typeRole, t);
325         item->setData(0, idRole, e.attribute("id"));
326         item->setToolTip(0, i18n("Duration mismatch"));
327     }
328
329     // Check missing proxies
330     max = missingProxies.count();
331     if (max > 0) {
332         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Proxy clip"));
333         item->setIcon(0, KIcon("dialog-warning"));
334         item->setText(1, i18n("%1 missing proxy clips, will be recreated on project opening", max));
335         item->setData(0, hashRole, e.attribute("file_hash"));
336         item->setData(0, statusRole, PROXYMISSING);
337         item->setToolTip(0, i18n("Missing proxy"));
338     }
339
340     for (int i = 0; i < max; i++) {
341         e = missingProxies.at(i).toElement();
342         QString clipType;
343         QString realPath = e.attribute("resource");
344         QString id = e.attribute("id");
345         // Tell Kdenlive to recreate proxy
346         e.setAttribute("_replaceproxy", "1");
347         // Replace proxy url with real clip in MLT producers
348         QDomNodeList properties;
349         QDomElement mltProd;
350         QDomElement property;
351         int prodsCount = documentProducers.count();
352         for (int j = 0; j < prodsCount; j++) {
353             mltProd = documentProducers.at(j).toElement();
354             QString prodId = mltProd.attribute("id");
355             bool slowmotion = false;
356             if (prodId.startsWith("slowmotion")) {
357                 slowmotion = true;
358                 prodId = prodId.section(':', 1, 1);
359             }
360             if (prodId.contains('_')) prodId = prodId.section('_', 0, 0);
361             if (prodId == id) {
362                 // Hit, we must replace url
363                 properties = mltProd.childNodes();
364                 for (int k = 0; k < properties.count(); ++k) {
365                     property = properties.item(k).toElement();
366                     if (property.attribute("name") == "resource") {
367                         QString resource = property.firstChild().nodeValue();                    
368                         QString suffix;
369                         if (slowmotion) suffix = '?' + resource.section('?', -1);
370                         property.firstChild().setNodeValue(realPath + suffix);
371                         break;
372                     }
373                 }
374             }
375         }
376     }
377     
378     if (max > 0) {
379         // original doc was modified
380         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
381         infoXml.setAttribute("modified", "1");
382     }
383     
384     // Check clips with available proxies but missing original source clips
385     max = missingSources.count();
386     if (max > 0) {
387         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Source clip"));
388         item->setIcon(0, KIcon("dialog-warning"));
389         item->setText(1, i18n("%1 missing source clips, you can only use the proxies", max));
390         item->setData(0, hashRole, e.attribute("file_hash"));
391         item->setData(0, statusRole, SOURCEMISSING);
392         item->setToolTip(0, i18n("Missing source clip"));
393         for (int i = 0; i < max; i++) {
394             e = missingSources.at(i).toElement();
395             QString clipType;
396             QString realPath = e.attribute("resource");
397             QString id = e.attribute("id");
398             // Tell Kdenlive the source is missing
399             e.setAttribute("_missingsource", "1");
400             QTreeWidgetItem *subitem = new QTreeWidgetItem(item, QStringList() << i18n("Source clip"));
401             kDebug()<<"// Adding missing source clip: "<<realPath;
402             subitem->setIcon(0, KIcon("dialog-close"));
403             subitem->setText(1, realPath);
404             subitem->setData(0, hashRole, e.attribute("file_hash"));
405             subitem->setData(0, sizeRole, e.attribute("file_size"));
406             subitem->setData(0, statusRole, CLIPMISSING);
407             int t = e.attribute("type").toInt();
408             subitem->setData(0, typeRole, t);
409             subitem->setData(0, idRole, id);
410         }
411     }
412     
413     if (max > 0) {
414         // original doc was modified
415         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
416         infoXml.setAttribute("modified", "1");
417     }
418     
419     connect(m_ui.recursiveSearch, SIGNAL(pressed()), this, SLOT(slotSearchClips()));
420     connect(m_ui.usePlaceholders, SIGNAL(pressed()), this, SLOT(slotPlaceholders()));
421     connect(m_ui.removeSelected, SIGNAL(pressed()), this, SLOT(slotDeleteSelected()));
422     connect(m_ui.fixDuration, SIGNAL(pressed()), this, SLOT(slotFixDuration()));
423     connect(m_ui.treeWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem *, int)), this, SLOT(slotEditItem(QTreeWidgetItem *, int)));
424     connect(m_ui.treeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(slotCheckButtons()));
425     //adjustSize();
426     if (m_ui.treeWidget->topLevelItem(0)) m_ui.treeWidget->setCurrentItem(m_ui.treeWidget->topLevelItem(0));
427     checkStatus();
428     int acceptMissing = m_dialog->exec();
429     if (acceptMissing == QDialog::Accepted) acceptDialog();
430     return (acceptMissing != QDialog::Accepted);
431 }
432
433 DocumentChecker::~DocumentChecker()
434 {
435     if (m_dialog) delete m_dialog;
436 }
437
438
439 QString DocumentChecker::getProperty(QDomElement effect, const QString &name)
440 {
441     QDomNodeList params = effect.elementsByTagName("property");
442     for (int i = 0; i < params.count(); i++) {
443         QDomElement e = params.item(i).toElement();
444         if (e.attribute("name") == name) {
445             return e.firstChild().nodeValue();
446         }
447     }
448     return QString();
449 }
450
451 void DocumentChecker::setProperty(QDomElement effect, const QString &name, const QString value)
452 {
453     QDomNodeList params = effect.elementsByTagName("property");
454     for (int i = 0; i < params.count(); i++) {
455         QDomElement e = params.item(i).toElement();
456         if (e.attribute("name") == name) {
457             e.firstChild().setNodeValue(value);
458         }
459     }
460 }
461
462 void DocumentChecker::slotSearchClips()
463 {
464     QString newpath = KFileDialog::getExistingDirectory(KUrl("kfiledialog:///clipfolder"), kapp->activeWindow(), i18n("Clips folder"));
465     if (newpath.isEmpty()) return;
466     int ix = 0;
467     bool fixed = false;
468     m_ui.recursiveSearch->setEnabled(false);
469     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
470     QDir searchDir(newpath);
471     while (child) {
472         if (child->data(0, statusRole).toInt() == SOURCEMISSING) {
473             for (int j = 0; j < child->childCount(); j++) {
474                 QTreeWidgetItem *subchild = child->child(j);
475                 QString clipPath = searchFileRecursively(searchDir, subchild->data(0, sizeRole).toString(), subchild->data(0, hashRole).toString());
476                 if (!clipPath.isEmpty()) {
477                     fixed = true;
478                     
479                     subchild->setText(1, clipPath);
480                     subchild->setIcon(0, KIcon("dialog-ok"));
481                     subchild->setData(0, statusRole, CLIPOK);
482                 }
483             }
484         }
485         else if (child->data(0, statusRole).toInt() == CLIPMISSING) {
486             QString clipPath = searchFileRecursively(searchDir, child->data(0, sizeRole).toString(), child->data(0, hashRole).toString());
487             if (!clipPath.isEmpty()) {
488                 fixed = true;
489                 child->setText(1, clipPath);
490                 child->setIcon(0, KIcon("dialog-ok"));
491                 child->setData(0, statusRole, CLIPOK);
492             }
493         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
494             QString fileName = searchLuma(searchDir, child->data(0, idRole).toString());
495             if (!fileName.isEmpty()) {
496                 fixed = true;
497                 child->setText(1, fileName);
498                 child->setIcon(0, KIcon("dialog-ok"));
499                 child->setData(0, statusRole, LUMAOK);
500             }
501         }
502         else if (child->data(0, typeRole).toInt() == TITLE_IMAGE_ELEMENT && child->data(0, statusRole).toInt() == CLIPPLACEHOLDER) {
503             // Search missing title images
504             QString missingFileName = KUrl(child->text(1)).fileName();
505             QString newPath = searchPathRecursively(searchDir, missingFileName);
506             if (!newPath.isEmpty()) {
507                 // File found
508                 fixed = true;
509                 child->setText(1, newPath);
510                 child->setIcon(0, KIcon("dialog-ok"));
511                 child->setData(0, statusRole, CLIPOK);
512             }
513         }
514         ix++;
515         child = m_ui.treeWidget->topLevelItem(ix);
516     }
517     m_ui.recursiveSearch->setEnabled(true);
518     if (fixed) {
519         // original doc was modified
520         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
521         infoXml.setAttribute("modified", "1");
522     }
523     checkStatus();
524 }
525
526
527 QString DocumentChecker::searchLuma(const QDir &dir, const QString &file) const
528 {
529     KUrl searchPath(KdenliveSettings::mltpath());
530     QString fname = KUrl(file).fileName();
531     if (file.contains("PAL"))
532         searchPath.cd("../lumas/PAL");
533     else
534         searchPath.cd("../lumas/NTSC");
535     QString result = searchPath.path(KUrl::AddTrailingSlash) + fname;
536     if (QFile::exists(result))
537         return result;
538     // try to find luma in application path
539     searchPath.clear();
540     searchPath = KUrl(QCoreApplication::applicationDirPath());
541     searchPath.cd("../share/apps/kdenlive/lumas");
542     result = searchPath.path(KUrl::AddTrailingSlash) + fname;
543     if (QFile::exists(result))
544         return result;
545     // Try in Kdenlive's standard KDE path
546     result = KStandardDirs::locate("appdata", "lumas/" + fname);
547     if (!result.isEmpty()) return result;
548     // Try in user's chosen folder 
549     return searchPathRecursively(dir, fname);
550 }
551
552 QString DocumentChecker::searchPathRecursively(const QDir &dir, const QString &fileName) const
553 {
554     QString foundFileName;
555     QStringList filters;
556     filters << fileName;
557     QDir searchDir(dir);
558     searchDir.setNameFilters(filters);
559     QStringList filesAndDirs = searchDir.entryList(QDir::Files | QDir::Readable);
560     if (!filesAndDirs.isEmpty()) return searchDir.absoluteFilePath(filesAndDirs.at(0));
561     searchDir.setNameFilters(QStringList());
562     filesAndDirs = searchDir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
563     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
564         foundFileName = searchPathRecursively(searchDir.absoluteFilePath(filesAndDirs.at(i)), fileName);
565         if (!foundFileName.isEmpty())
566             break;
567     }
568     return foundFileName;
569 }
570
571 QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const
572 {
573     QString foundFileName;
574     QByteArray fileData;
575     QByteArray fileHash;
576     QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable);
577     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
578         QFile file(dir.absoluteFilePath(filesAndDirs.at(i)));
579         if (QString::number(file.size()) == matchSize) {
580             if (file.open(QIODevice::ReadOnly)) {
581                 /*
582                 * 1 MB = 1 second per 450 files (or faster)
583                 * 10 MB = 9 seconds per 450 files (or faster)
584                 */
585                 if (file.size() > 1000000 * 2) {
586                     fileData = file.read(1000000);
587                     if (file.seek(file.size() - 1000000))
588                         fileData.append(file.readAll());
589                 } else
590                     fileData = file.readAll();
591                 file.close();
592                 fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
593                 if (QString(fileHash.toHex()) == matchHash)
594                     return file.fileName();
595             }
596         }
597         //kDebug() << filesAndDirs.at(i) << file.size() << fileHash.toHex();
598     }
599     filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
600     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
601         foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash);
602         if (!foundFileName.isEmpty())
603             break;
604     }
605     return foundFileName;
606 }
607
608 void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int)
609 {
610     int t = item->data(0, typeRole).toInt();
611     if (t == TITLE_FONT_ELEMENT || t == UNKNOWN) return;
612     //|| t == TITLE_IMAGE_ELEMENT) {
613
614     KUrl url = KUrlRequesterDialog::getUrl(item->text(1), m_dialog, i18n("Enter new location for file"));
615     if (url.isEmpty()) return;
616     item->setText(1, url.path());
617     if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0)) {
618         item->setIcon(0, KIcon("dialog-ok"));
619         int id = item->data(0, statusRole).toInt();
620         if (id < 10) item->setData(0, statusRole, CLIPOK);
621         else item->setData(0, statusRole, LUMAOK);
622         checkStatus();
623     } else {
624         item->setIcon(0, KIcon("dialog-close"));
625         int id = item->data(0, statusRole).toInt();
626         if (id < 10) item->setData(0, statusRole, CLIPMISSING);
627         else item->setData(0, statusRole, LUMAMISSING);
628         checkStatus();
629     }
630 }
631
632
633 void DocumentChecker::acceptDialog()
634 {
635     QDomNodeList producers = m_doc.elementsByTagName("producer");
636     QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
637     int ix = 0;
638
639     // prepare transitions
640     QDomNodeList trans = m_doc.elementsByTagName("transition");
641
642     // Mark document as modified
643     m_doc.documentElement().setAttribute("modified", 1);
644
645     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
646     while (child) {
647         if (child->data(0, statusRole).toInt() == SOURCEMISSING) {
648             for (int j = 0; j < child->childCount(); j++) {
649                 fixClipItem(child->child(j), producers, infoproducers, trans);
650             }
651         }
652         else fixClipItem(child, producers, infoproducers, trans);
653         ix++;
654         child = m_ui.treeWidget->topLevelItem(ix);
655     }
656     //QDialog::accept();
657 }
658
659 void DocumentChecker::fixClipItem(QTreeWidgetItem *child, QDomNodeList producers, QDomNodeList infoproducers, QDomNodeList trans)
660 {
661     QDomElement e, property;
662     QDomNodeList properties;
663     int t = child->data(0, typeRole).toInt();
664     if (child->data(0, statusRole).toInt() == CLIPOK) {
665         QString id = child->data(0, idRole).toString();
666         if (t == TITLE_IMAGE_ELEMENT) {
667             // edit images embedded in titles
668             for (int i = 0; i < infoproducers.count(); i++) {
669                 e = infoproducers.item(i).toElement();
670                 if (e.attribute("id") == id) {
671                     // Fix clip
672                     QString xml = e.attribute("xmldata");
673                     xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
674                     e.setAttribute("xmldata", xml);
675                     break;
676                 }
677             }
678             for (int i = 0; i < producers.count(); i++) {
679                 e = producers.item(i).toElement();
680                 if (e.attribute("id").section('_', 0, 0) == id) {
681                     // Fix clip
682                     properties = e.childNodes();
683                     for (int j = 0; j < properties.count(); ++j) {
684                         property = properties.item(j).toElement();
685                         if (property.attribute("name") == "xmldata") {
686                             QString xml = property.firstChild().nodeValue();
687                             xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
688                             property.firstChild().setNodeValue(xml);
689                             break;
690                         }
691                     }
692                 }
693             }
694         } else {
695             // edit clip url
696             for (int i = 0; i < infoproducers.count(); i++) {
697                 e = infoproducers.item(i).toElement();
698                 if (e.attribute("id") == id) {
699                     // Fix clip
700                     e.setAttribute("resource", child->text(1));
701                     e.setAttribute("name", KUrl(child->text(1)).fileName());
702                     e.removeAttribute("_missingsource");
703                     break;
704                 }
705             }
706             for (int i = 0; i < producers.count(); i++) {
707                 e = producers.item(i).toElement();
708                 if (e.attribute("id").section('_', 0, 0) == id || e.attribute("id").section(':', 1, 1) == id) {
709                     // Fix clip
710                     properties = e.childNodes();
711                     for (int j = 0; j < properties.count(); ++j) {
712                         property = properties.item(j).toElement();
713                         if (property.attribute("name") == "resource") {
714                             QString resource = property.firstChild().nodeValue();
715                             if (resource.contains(QRegExp("\\?[0-9]+\\.[0-9]+(&amp;strobe=[0-9]+)?$")))
716                                 property.firstChild().setNodeValue(child->text(1) + '?' + resource.section('?', -1));
717                             else
718                                 property.firstChild().setNodeValue(child->text(1));
719                             break;
720                         }
721                     }
722                 }
723             }
724         }
725     } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER && t != TITLE_FONT_ELEMENT && t != TITLE_IMAGE_ELEMENT) {
726         QString id = child->data(0, idRole).toString();
727         for (int i = 0; i < infoproducers.count(); i++) {
728             e = infoproducers.item(i).toElement();
729             if (e.attribute("id") == id) {
730                 // Fix clip
731                 e.setAttribute("placeholder", '1');
732                 break;
733             }
734         }
735     } else if (child->data(0, statusRole).toInt() == LUMAOK) {
736         for (int i = 0; i < trans.count(); i++) {
737             QString luma = getProperty(trans.at(i).toElement(), "luma");
738             if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
739                 setProperty(trans.at(i).toElement(), "luma", child->text(1));
740                 kDebug() << "replace with; " << child->text(1);
741             }
742         }
743     } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
744         for (int i = 0; i < trans.count(); i++) {
745             QString luma = getProperty(trans.at(i).toElement(), "luma");
746             if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
747                 setProperty(trans.at(i).toElement(), "luma", QString());
748             }
749         }
750     }
751 }
752
753 void DocumentChecker::slotPlaceholders()
754 {
755     int ix = 0;
756     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
757     while (child) {
758         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
759             child->setData(0, statusRole, CLIPPLACEHOLDER);
760             child->setIcon(0, KIcon("dialog-ok"));
761         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
762             child->setData(0, statusRole, LUMAPLACEHOLDER);
763             child->setIcon(0, KIcon("dialog-ok"));
764         }
765         ix++;
766         child = m_ui.treeWidget->topLevelItem(ix);
767     }
768     checkStatus();
769 }
770
771 void DocumentChecker::slotFixDuration()
772 {
773     int ix = 0;
774     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
775     QDomNodeList documentProducers = m_doc.elementsByTagName("producer");
776     while (child) {
777         if (child->data(0, statusRole).toInt() == CLIPWRONGDURATION) {
778             QString id = child->data(0, idRole).toString();
779             bool resetDuration = child->data(0, resetDurationRole).toInt();
780
781             for (int i = 0; i < m_info.count(); i++) {
782                 QDomElement e = m_info.at(i).toElement();
783                 if (e.attribute("id") == id) {
784                     if (m_missingClips.contains(e)) {
785                         // we cannot fix duration of missing clips
786                         resetDuration = false;
787                     }
788                     else {
789                         if (resetDuration) e.removeAttribute("duration");
790                         else e.setAttribute("duration", child->data(0, sizeRole).toString());
791                         child->setData(0, statusRole, CLIPOK);
792                         child->setIcon(0, KIcon("dialog-ok"));
793                     }
794                     break;
795                 }
796             }
797             if (resetDuration) {
798                 // something is wrong in clip durations, so remove them so mlt fetches them again
799                 for (int j = 0; j < documentProducers.count(); j++) {
800                     QDomElement mltProd = documentProducers.at(j).toElement();
801                     QString prodId = mltProd.attribute("id");
802                     if (prodId == id || prodId.startsWith(id + '_')) {
803                         EffectsList::removeProperty(mltProd, "length");
804                     }
805                 }
806             }
807         }
808         ix++;
809         child = m_ui.treeWidget->topLevelItem(ix);
810     }
811     QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
812     infoXml.setAttribute("modified", "1");
813     m_ui.fixDuration->setEnabled(false);
814     checkStatus();
815 }
816
817
818 void DocumentChecker::checkStatus()
819 {
820     bool status = true;
821     int ix = 0;
822     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
823     while (child) {
824         int status = child->data(0, statusRole).toInt();
825         if (status == CLIPMISSING || status == LUMAMISSING || status == CLIPWRONGDURATION) {
826             status = false;
827             break;
828         }
829         ix++;
830         child = m_ui.treeWidget->topLevelItem(ix);
831     }
832     m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status);
833 }
834
835
836 void DocumentChecker::slotDeleteSelected()
837 {
838     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)
839         return;
840     QStringList deletedIds;
841     QStringList deletedLumas;
842     QDomNodeList playlists = m_doc.elementsByTagName("playlist");
843
844     foreach(QTreeWidgetItem *child, m_ui.treeWidget->selectedItems()) {
845         int id = child->data(0, statusRole).toInt();
846         if (id == CLIPMISSING) {
847             deletedIds.append(child->data(0, idRole).toString());
848             delete child;
849         }
850         else if (id == LUMAMISSING) {
851             deletedLumas.append(child->data(0, idRole).toString());
852             delete child;
853         }
854     }
855
856     if (!deletedLumas.isEmpty()) {
857         QDomElement e;
858         QDomNodeList transitions = m_doc.elementsByTagName("transition");
859         foreach (QString lumaPath, deletedLumas) {
860             for (int i = 0; i < transitions.count(); i++) {
861                 e = transitions.item(i).toElement();
862                 QString resource = EffectsList::property(e, "luma");
863                 if (resource == lumaPath) EffectsList::removeProperty(e, "luma");
864             }
865         }
866     }
867
868     if (!deletedIds.isEmpty()) {
869         QDomElement e;
870         QDomNodeList producers = m_doc.elementsByTagName("producer");
871         QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
872
873         QDomNode mlt = m_doc.elementsByTagName("mlt").at(0);
874         QDomNode kdenlivedoc = m_doc.elementsByTagName("kdenlivedoc").at(0);
875
876         for (int i = 0, j = 0; i < infoproducers.count() && j < deletedIds.count(); i++) {
877             e = infoproducers.item(i).toElement();
878             if (deletedIds.contains(e.attribute("id"))) {
879                 // Remove clip
880                 kdenlivedoc.removeChild(e);
881                 i--;
882                 j++;
883             }
884         }
885
886         for (int i = 0; i < producers.count(); i++) {
887             e = producers.item(i).toElement();
888             if (deletedIds.contains(e.attribute("id").section('_', 0, 0)) || deletedIds.contains(e.attribute("id").section(':', 1, 1).section('_', 0, 0))) {
889                 // Remove clip
890                 mlt.removeChild(e);
891                 i--;
892             }
893         }
894
895         for (int i = 0; i < playlists.count(); i++) {
896             QDomNodeList entries = playlists.at(i).toElement().elementsByTagName("entry");
897             for (int j = 0; j < entries.count(); j++) {
898                 e = entries.item(j).toElement();
899                 if (deletedIds.contains(e.attribute("producer").section('_', 0, 0)) || deletedIds.contains(e.attribute("producer").section(':', 1, 1).section('_', 0, 0))) {
900                     // Replace clip with blank
901                     while (e.childNodes().count() > 0)
902                         e.removeChild(e.firstChild());
903                     e.setTagName("blank");
904                     e.removeAttribute("producer");
905                     int length = e.attribute("out").toInt() - e.attribute("in").toInt();
906                     e.setAttribute("length", length);
907                     j--;
908                 }
909             }
910         }
911         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
912         infoXml.setAttribute("modified", "1");
913         checkStatus();
914     }
915 }
916
917 void DocumentChecker::checkMissingImagesAndFonts(QStringList images, QStringList fonts, const QString &id, const QString &baseClip)
918 {
919     QDomDocument doc;
920     foreach(const QString &img, images) {
921         if (m_safeImages.contains(img)) continue;
922         if (!KIO::NetAccess::exists(KUrl(img), KIO::NetAccess::SourceSide, 0)) {
923             QDomElement e = doc.createElement("missingclip");
924             e.setAttribute("type", TITLE_IMAGE_ELEMENT);
925             e.setAttribute("resource", img);
926             e.setAttribute("id", id);
927             e.setAttribute("name", baseClip);
928             m_missingClips.append(e);
929         }
930         else m_safeImages.append(img);
931     }
932     foreach(const QString &fontelement, fonts) {
933         if (m_safeFonts.contains(fontelement)) continue;
934         QFont f(fontelement);
935         //kDebug() << "/ / / CHK FONTS: " << fontelement << " = " << QFontInfo(f).family();
936         if (fontelement != QFontInfo(f).family()) {
937             QDomElement e = doc.createElement("missingclip");
938             e.setAttribute("type", TITLE_FONT_ELEMENT);
939             e.setAttribute("resource", fontelement);
940             e.setAttribute("id", id);
941             e.setAttribute("name", baseClip);
942             m_missingClips.append(e);
943         }
944         else m_safeFonts.append(fontelement);
945     }
946 }
947
948
949 void DocumentChecker::slotCheckButtons()
950 {
951     if (m_ui.treeWidget->currentItem()) {
952         QTreeWidgetItem *item = m_ui.treeWidget->currentItem();
953         int t = item->data(0, typeRole).toInt();
954         int s = item->data(0, statusRole).toInt();
955         if (t == TITLE_FONT_ELEMENT || t == TITLE_IMAGE_ELEMENT || s == PROXYMISSING) {
956             m_ui.removeSelected->setEnabled(false);
957         } else m_ui.removeSelected->setEnabled(true);
958     }
959
960 }
961
962 #include "documentchecker.moc"
963
964