]> git.sesse.net Git - kdenlive/blob - src/documentchecker.cpp
da32ec340468be04db55f9d57b8081bf9ade1a9b
[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 "widgets/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(const QDomNodeList &infoproducers, const 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 realPath = e.attribute("resource");
343         QString id = e.attribute("id");
344         // Tell Kdenlive to recreate proxy
345         e.setAttribute("_replaceproxy", "1");
346         // Replace proxy url with real clip in MLT producers
347         QDomNodeList properties;
348         QDomElement mltProd;
349         QDomElement property;
350         int prodsCount = documentProducers.count();
351         for (int j = 0; j < prodsCount; j++) {
352             mltProd = documentProducers.at(j).toElement();
353             QString prodId = mltProd.attribute("id");
354             bool slowmotion = false;
355             if (prodId.startsWith("slowmotion")) {
356                 slowmotion = true;
357                 prodId = prodId.section(':', 1, 1);
358             }
359             if (prodId.contains('_')) prodId = prodId.section('_', 0, 0);
360             if (prodId == id) {
361                 // Hit, we must replace url
362                 properties = mltProd.childNodes();
363                 for (int k = 0; k < properties.count(); ++k) {
364                     property = properties.item(k).toElement();
365                     if (property.attribute("name") == "resource") {
366                         QString resource = property.firstChild().nodeValue();                    
367                         QString suffix;
368                         if (slowmotion) suffix = '?' + resource.section('?', -1);
369                         property.firstChild().setNodeValue(realPath + suffix);
370                         break;
371                     }
372                 }
373             }
374         }
375     }
376     
377     if (max > 0) {
378         // original doc was modified
379         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
380         infoXml.setAttribute("modified", "1");
381     }
382     
383     // Check clips with available proxies but missing original source clips
384     max = missingSources.count();
385     if (max > 0) {
386         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Source clip"));
387         item->setIcon(0, KIcon("dialog-warning"));
388         item->setText(1, i18n("%1 missing source clips, you can only use the proxies", max));
389         item->setData(0, hashRole, e.attribute("file_hash"));
390         item->setData(0, statusRole, SOURCEMISSING);
391         item->setToolTip(0, i18n("Missing source clip"));
392         for (int i = 0; i < max; ++i) {
393             e = missingSources.at(i).toElement();
394             QString clipType;
395             QString realPath = e.attribute("resource");
396             QString id = e.attribute("id");
397             // Tell Kdenlive the source is missing
398             e.setAttribute("_missingsource", "1");
399             QTreeWidgetItem *subitem = new QTreeWidgetItem(item, QStringList() << i18n("Source clip"));
400             kDebug()<<"// Adding missing source clip: "<<realPath;
401             subitem->setIcon(0, KIcon("dialog-close"));
402             subitem->setText(1, realPath);
403             subitem->setData(0, hashRole, e.attribute("file_hash"));
404             subitem->setData(0, sizeRole, e.attribute("file_size"));
405             subitem->setData(0, statusRole, CLIPMISSING);
406             int t = e.attribute("type").toInt();
407             subitem->setData(0, typeRole, t);
408             subitem->setData(0, idRole, id);
409         }
410     }
411     
412     if (max > 0) {
413         // original doc was modified
414         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
415         infoXml.setAttribute("modified", "1");
416     }
417     
418     connect(m_ui.recursiveSearch, SIGNAL(pressed()), this, SLOT(slotSearchClips()));
419     connect(m_ui.usePlaceholders, SIGNAL(pressed()), this, SLOT(slotPlaceholders()));
420     connect(m_ui.removeSelected, SIGNAL(pressed()), this, SLOT(slotDeleteSelected()));
421     connect(m_ui.fixDuration, SIGNAL(pressed()), this, SLOT(slotFixDuration()));
422     connect(m_ui.treeWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(slotEditItem(QTreeWidgetItem*,int)));
423     connect(m_ui.treeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(slotCheckButtons()));
424     //adjustSize();
425     if (m_ui.treeWidget->topLevelItem(0)) m_ui.treeWidget->setCurrentItem(m_ui.treeWidget->topLevelItem(0));
426     checkStatus();
427     int acceptMissing = m_dialog->exec();
428     if (acceptMissing == QDialog::Accepted) acceptDialog();
429     return (acceptMissing != QDialog::Accepted);
430 }
431
432 DocumentChecker::~DocumentChecker()
433 {
434     delete m_dialog;
435 }
436
437
438 QString DocumentChecker::getProperty(QDomElement effect, const QString &name)
439 {
440     QDomNodeList params = effect.elementsByTagName("property");
441     for (int i = 0; i < params.count(); ++i) {
442         QDomElement e = params.item(i).toElement();
443         if (e.attribute("name") == name) {
444             return e.firstChild().nodeValue();
445         }
446     }
447     return QString();
448 }
449
450 void DocumentChecker::setProperty(QDomElement effect, const QString &name, const QString &value)
451 {
452     QDomNodeList params = effect.elementsByTagName("property");
453     for (int i = 0; i < params.count(); ++i) {
454         QDomElement e = params.item(i).toElement();
455         if (e.attribute("name") == name) {
456             e.firstChild().setNodeValue(value);
457         }
458     }
459 }
460
461 void DocumentChecker::slotSearchClips()
462 {
463     QString newpath = KFileDialog::getExistingDirectory(KUrl("kfiledialog:///clipfolder"), kapp->activeWindow(), i18n("Clips folder"));
464     if (newpath.isEmpty()) return;
465     int ix = 0;
466     bool fixed = false;
467     m_ui.recursiveSearch->setChecked(true);
468     qApp->processEvents();
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->setChecked(false);
518     m_ui.recursiveSearch->setEnabled(true);
519     if (fixed) {
520         // original doc was modified
521         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
522         infoXml.setAttribute("modified", "1");
523     }
524     checkStatus();
525 }
526
527
528 QString DocumentChecker::searchLuma(const QDir &dir, const QString &file) const
529 {
530     KUrl searchPath(KdenliveSettings::mltpath());
531     QString fname = KUrl(file).fileName();
532     if (file.contains("PAL"))
533         searchPath.cd("../lumas/PAL");
534     else
535         searchPath.cd("../lumas/NTSC");
536     QString result = searchPath.path(KUrl::AddTrailingSlash) + fname;
537     if (QFile::exists(result))
538         return result;
539     // try to find luma in application path
540     searchPath.clear();
541     searchPath = KUrl(QCoreApplication::applicationDirPath());
542     searchPath.cd("../share/apps/kdenlive/lumas");
543     result = searchPath.path(KUrl::AddTrailingSlash) + fname;
544     if (QFile::exists(result))
545         return result;
546     // Try in Kdenlive's standard KDE path
547     result = KStandardDirs::locate("appdata", "lumas/" + fname);
548     if (!result.isEmpty()) return result;
549     // Try in user's chosen folder 
550     return searchPathRecursively(dir, fname);
551 }
552
553 QString DocumentChecker::searchPathRecursively(const QDir &dir, const QString &fileName) const
554 {
555     QString foundFileName;
556     QStringList filters;
557     filters << fileName;
558     QDir searchDir(dir);
559     searchDir.setNameFilters(filters);
560     QStringList filesAndDirs = searchDir.entryList(QDir::Files | QDir::Readable);
561     if (!filesAndDirs.isEmpty()) return searchDir.absoluteFilePath(filesAndDirs.at(0));
562     searchDir.setNameFilters(QStringList());
563     filesAndDirs = searchDir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
564     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) {
565         foundFileName = searchPathRecursively(searchDir.absoluteFilePath(filesAndDirs.at(i)), fileName);
566         if (!foundFileName.isEmpty())
567             break;
568     }
569     return foundFileName;
570 }
571
572 QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const
573 {
574     QString foundFileName;
575     QByteArray fileData;
576     QByteArray fileHash;
577     QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable);
578     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) {
579         QFile file(dir.absoluteFilePath(filesAndDirs.at(i)));
580         if (QString::number(file.size()) == matchSize) {
581             if (file.open(QIODevice::ReadOnly)) {
582                 /*
583                 * 1 MB = 1 second per 450 files (or faster)
584                 * 10 MB = 9 seconds per 450 files (or faster)
585                 */
586                 if (file.size() > 1000000 * 2) {
587                     fileData = file.read(1000000);
588                     if (file.seek(file.size() - 1000000))
589                         fileData.append(file.readAll());
590                 } else
591                     fileData = file.readAll();
592                 file.close();
593                 fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
594                 if (QString(fileHash.toHex()) == matchHash)
595                     return file.fileName();
596             }
597         }
598         //kDebug() << filesAndDirs.at(i) << file.size() << fileHash.toHex();
599     }
600     filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
601     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) {
602         foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash);
603         if (!foundFileName.isEmpty())
604             break;
605     }
606     return foundFileName;
607 }
608
609 void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int)
610 {
611     int t = item->data(0, typeRole).toInt();
612     if (t == TITLE_FONT_ELEMENT || t == UNKNOWN) return;
613     //|| t == TITLE_IMAGE_ELEMENT) {
614
615     KUrl url = KUrlRequesterDialog::getUrl(item->text(1), m_dialog, i18n("Enter new location for file"));
616     if (url.isEmpty()) return;
617     item->setText(1, url.path());
618     if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0)) {
619         item->setIcon(0, KIcon("dialog-ok"));
620         int id = item->data(0, statusRole).toInt();
621         if (id < 10) item->setData(0, statusRole, CLIPOK);
622         else item->setData(0, statusRole, LUMAOK);
623         checkStatus();
624     } else {
625         item->setIcon(0, KIcon("dialog-close"));
626         int id = item->data(0, statusRole).toInt();
627         if (id < 10) item->setData(0, statusRole, CLIPMISSING);
628         else item->setData(0, statusRole, LUMAMISSING);
629         checkStatus();
630     }
631 }
632
633
634 void DocumentChecker::acceptDialog()
635 {
636     QDomNodeList producers = m_doc.elementsByTagName("producer");
637     QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
638     int ix = 0;
639
640     // prepare transitions
641     QDomNodeList trans = m_doc.elementsByTagName("transition");
642
643     // Mark document as modified
644     m_doc.documentElement().setAttribute("modified", 1);
645
646     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
647     while (child) {
648         if (child->data(0, statusRole).toInt() == SOURCEMISSING) {
649             for (int j = 0; j < child->childCount(); j++) {
650                 fixClipItem(child->child(j), producers, infoproducers, trans);
651             }
652         }
653         else fixClipItem(child, producers, infoproducers, trans);
654         ix++;
655         child = m_ui.treeWidget->topLevelItem(ix);
656     }
657     //QDialog::accept();
658 }
659
660 void DocumentChecker::fixClipItem(QTreeWidgetItem *child, QDomNodeList producers, QDomNodeList infoproducers, QDomNodeList trans)
661 {
662     QDomElement e, property;
663     QDomNodeList properties;
664     int t = child->data(0, typeRole).toInt();
665     if (child->data(0, statusRole).toInt() == CLIPOK) {
666         QString id = child->data(0, idRole).toString();
667         if (t == TITLE_IMAGE_ELEMENT) {
668             // edit images embedded in titles
669             for (int i = 0; i < infoproducers.count(); ++i) {
670                 e = infoproducers.item(i).toElement();
671                 if (e.attribute("id") == id) {
672                     // Fix clip
673                     QString xml = e.attribute("xmldata");
674                     xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
675                     e.setAttribute("xmldata", xml);
676                     break;
677                 }
678             }
679             for (int i = 0; i < producers.count(); ++i) {
680                 e = producers.item(i).toElement();
681                 if (e.attribute("id").section('_', 0, 0) == id) {
682                     // Fix clip
683                     properties = e.childNodes();
684                     for (int j = 0; j < properties.count(); ++j) {
685                         property = properties.item(j).toElement();
686                         if (property.attribute("name") == "xmldata") {
687                             QString xml = property.firstChild().nodeValue();
688                             xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
689                             property.firstChild().setNodeValue(xml);
690                             break;
691                         }
692                     }
693                 }
694             }
695         } else {
696             // edit clip url
697             for (int i = 0; i < infoproducers.count(); ++i) {
698                 e = infoproducers.item(i).toElement();
699                 if (e.attribute("id") == id) {
700                     // Fix clip
701                     e.setAttribute("resource", child->text(1));
702                     e.setAttribute("name", KUrl(child->text(1)).fileName());
703                     e.removeAttribute("_missingsource");
704                     break;
705                 }
706             }
707             for (int i = 0; i < producers.count(); ++i) {
708                 e = producers.item(i).toElement();
709                 if (e.attribute("id").section('_', 0, 0) == id || e.attribute("id").section(':', 1, 1) == id) {
710                     // Fix clip
711                     properties = e.childNodes();
712                     for (int j = 0; j < properties.count(); ++j) {
713                         property = properties.item(j).toElement();
714                         if (property.attribute("name") == "resource") {
715                             QString resource = property.firstChild().nodeValue();
716                             if (resource.contains(QRegExp("\\?[0-9]+\\.[0-9]+(&amp;strobe=[0-9]+)?$")))
717                                 property.firstChild().setNodeValue(child->text(1) + '?' + resource.section('?', -1));
718                             else
719                                 property.firstChild().setNodeValue(child->text(1));
720                             break;
721                         }
722                     }
723                 }
724             }
725         }
726     } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER && t != TITLE_FONT_ELEMENT && t != TITLE_IMAGE_ELEMENT) {
727         QString id = child->data(0, idRole).toString();
728         for (int i = 0; i < infoproducers.count(); ++i) {
729             e = infoproducers.item(i).toElement();
730             if (e.attribute("id") == id) {
731                 // Fix clip
732                 e.setAttribute("placeholder", '1');
733                 break;
734             }
735         }
736     } else if (child->data(0, statusRole).toInt() == LUMAOK) {
737         for (int i = 0; i < trans.count(); ++i) {
738             QString luma = getProperty(trans.at(i).toElement(), "luma");
739             if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
740                 setProperty(trans.at(i).toElement(), "luma", child->text(1));
741                 kDebug() << "replace with; " << child->text(1);
742             }
743         }
744     } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
745         for (int i = 0; i < trans.count(); ++i) {
746             QString luma = getProperty(trans.at(i).toElement(), "luma");
747             if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
748                 setProperty(trans.at(i).toElement(), "luma", QString());
749             }
750         }
751     }
752 }
753
754 void DocumentChecker::slotPlaceholders()
755 {
756     int ix = 0;
757     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
758     while (child) {
759         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
760             child->setData(0, statusRole, CLIPPLACEHOLDER);
761             child->setIcon(0, KIcon("dialog-ok"));
762         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
763             child->setData(0, statusRole, LUMAPLACEHOLDER);
764             child->setIcon(0, KIcon("dialog-ok"));
765         }
766         ix++;
767         child = m_ui.treeWidget->topLevelItem(ix);
768     }
769     checkStatus();
770 }
771
772 void DocumentChecker::slotFixDuration()
773 {
774     int ix = 0;
775     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
776     QDomNodeList documentProducers = m_doc.elementsByTagName("producer");
777     while (child) {
778         if (child->data(0, statusRole).toInt() == CLIPWRONGDURATION) {
779             QString id = child->data(0, idRole).toString();
780             bool resetDuration = child->data(0, resetDurationRole).toInt();
781
782             for (int i = 0; i < m_info.count(); ++i) {
783                 QDomElement e = m_info.at(i).toElement();
784                 if (e.attribute("id") == id) {
785                     if (m_missingClips.contains(e)) {
786                         // we cannot fix duration of missing clips
787                         resetDuration = false;
788                     }
789                     else {
790                         if (resetDuration) e.removeAttribute("duration");
791                         else e.setAttribute("duration", child->data(0, sizeRole).toString());
792                         child->setData(0, statusRole, CLIPOK);
793                         child->setIcon(0, KIcon("dialog-ok"));
794                     }
795                     break;
796                 }
797             }
798             if (resetDuration) {
799                 // something is wrong in clip durations, so remove them so mlt fetches them again
800                 for (int j = 0; j < documentProducers.count(); j++) {
801                     QDomElement mltProd = documentProducers.at(j).toElement();
802                     QString prodId = mltProd.attribute("id");
803                     if (prodId == id || prodId.startsWith(id + '_')) {
804                         EffectsList::removeProperty(mltProd, "length");
805                     }
806                 }
807             }
808         }
809         ix++;
810         child = m_ui.treeWidget->topLevelItem(ix);
811     }
812     QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
813     infoXml.setAttribute("modified", "1");
814     m_ui.fixDuration->setEnabled(false);
815     checkStatus();
816 }
817
818
819 void DocumentChecker::checkStatus()
820 {
821     bool status = true;
822     int ix = 0;
823     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
824     while (child) {
825         int childStatus = child->data(0, statusRole).toInt();
826         if (childStatus == CLIPMISSING || childStatus == LUMAMISSING || childStatus == CLIPWRONGDURATION) {
827             status = false;
828             break;
829         }
830         ix++;
831         child = m_ui.treeWidget->topLevelItem(ix);
832     }
833     m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status);
834 }
835
836
837 void DocumentChecker::slotDeleteSelected()
838 {
839     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)
840         return;
841     QStringList deletedIds;
842     QStringList deletedLumas;
843     QDomNodeList playlists = m_doc.elementsByTagName("playlist");
844
845     foreach(QTreeWidgetItem *child, m_ui.treeWidget->selectedItems()) {
846         int id = child->data(0, statusRole).toInt();
847         if (id == CLIPMISSING) {
848             deletedIds.append(child->data(0, idRole).toString());
849             delete child;
850         }
851         else if (id == LUMAMISSING) {
852             deletedLumas.append(child->data(0, idRole).toString());
853             delete child;
854         }
855     }
856
857     if (!deletedLumas.isEmpty()) {
858         QDomElement e;
859         QDomNodeList transitions = m_doc.elementsByTagName("transition");
860         foreach (const QString &lumaPath, deletedLumas) {
861             for (int i = 0; i < transitions.count(); ++i) {
862                 e = transitions.item(i).toElement();
863                 QString resource = EffectsList::property(e, "luma");
864                 if (resource == lumaPath) EffectsList::removeProperty(e, "luma");
865             }
866         }
867     }
868
869     if (!deletedIds.isEmpty()) {
870         QDomElement e;
871         QDomNodeList producers = m_doc.elementsByTagName("producer");
872         QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
873
874         QDomNode mlt = m_doc.elementsByTagName("mlt").at(0);
875         QDomNode kdenlivedoc = m_doc.elementsByTagName("kdenlivedoc").at(0);
876
877         for (int i = 0, j = 0; i < infoproducers.count() && j < deletedIds.count(); ++i) {
878             e = infoproducers.item(i).toElement();
879             if (deletedIds.contains(e.attribute("id"))) {
880                 // Remove clip
881                 kdenlivedoc.removeChild(e);
882                 --i;
883                 j++;
884             }
885         }
886
887         for (int i = 0; i < producers.count(); ++i) {
888             e = producers.item(i).toElement();
889             if (deletedIds.contains(e.attribute("id").section('_', 0, 0)) || deletedIds.contains(e.attribute("id").section(':', 1, 1).section('_', 0, 0))) {
890                 // Remove clip
891                 mlt.removeChild(e);
892                 --i;
893             }
894         }
895
896         for (int i = 0; i < playlists.count(); ++i) {
897             QDomNodeList entries = playlists.at(i).toElement().elementsByTagName("entry");
898             for (int j = 0; j < entries.count(); j++) {
899                 e = entries.item(j).toElement();
900                 if (deletedIds.contains(e.attribute("producer").section('_', 0, 0)) || deletedIds.contains(e.attribute("producer").section(':', 1, 1).section('_', 0, 0))) {
901                     // Replace clip with blank
902                     while (e.childNodes().count() > 0)
903                         e.removeChild(e.firstChild());
904                     e.setTagName("blank");
905                     e.removeAttribute("producer");
906                     int length = e.attribute("out").toInt() - e.attribute("in").toInt();
907                     e.setAttribute("length", length);
908                     j--;
909                 }
910             }
911         }
912         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
913         infoXml.setAttribute("modified", "1");
914         checkStatus();
915     }
916 }
917
918 void DocumentChecker::checkMissingImagesAndFonts(const QStringList &images, const QStringList &fonts, const QString &id, const QString &baseClip)
919 {
920     QDomDocument doc;
921     foreach(const QString &img, images) {
922         if (m_safeImages.contains(img)) continue;
923         if (!KIO::NetAccess::exists(KUrl(img), KIO::NetAccess::SourceSide, 0)) {
924             QDomElement e = doc.createElement("missingclip");
925             e.setAttribute("type", TITLE_IMAGE_ELEMENT);
926             e.setAttribute("resource", img);
927             e.setAttribute("id", id);
928             e.setAttribute("name", baseClip);
929             m_missingClips.append(e);
930         }
931         else m_safeImages.append(img);
932     }
933     foreach(const QString &fontelement, fonts) {
934         if (m_safeFonts.contains(fontelement)) continue;
935         QFont f(fontelement);
936         //kDebug() << "/ / / CHK FONTS: " << fontelement << " = " << QFontInfo(f).family();
937         if (fontelement != QFontInfo(f).family()) {
938             QDomElement e = doc.createElement("missingclip");
939             e.setAttribute("type", TITLE_FONT_ELEMENT);
940             e.setAttribute("resource", fontelement);
941             e.setAttribute("id", id);
942             e.setAttribute("name", baseClip);
943             m_missingClips.append(e);
944         }
945         else m_safeFonts.append(fontelement);
946     }
947 }
948
949
950 void DocumentChecker::slotCheckButtons()
951 {
952     if (m_ui.treeWidget->currentItem()) {
953         QTreeWidgetItem *item = m_ui.treeWidget->currentItem();
954         int t = item->data(0, typeRole).toInt();
955         int s = item->data(0, statusRole).toInt();
956         if (t == TITLE_FONT_ELEMENT || t == TITLE_IMAGE_ELEMENT || s == PROXYMISSING) {
957             m_ui.removeSelected->setEnabled(false);
958         } else m_ui.removeSelected->setEnabled(true);
959     }
960
961 }
962
963 #include "documentchecker.moc"
964
965