]> git.sesse.net Git - kdenlive/blob - src/documentchecker.cpp
96f63c8dc79143ee7011c87c9f99212dc8f903a7
[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(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 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->setChecked(true);
469     qApp->processEvents();
470     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
471     QDir searchDir(newpath);
472     while (child) {
473         if (child->data(0, statusRole).toInt() == SOURCEMISSING) {
474             for (int j = 0; j < child->childCount(); j++) {
475                 QTreeWidgetItem *subchild = child->child(j);
476                 QString clipPath = searchFileRecursively(searchDir, subchild->data(0, sizeRole).toString(), subchild->data(0, hashRole).toString());
477                 if (!clipPath.isEmpty()) {
478                     fixed = true;
479                     
480                     subchild->setText(1, clipPath);
481                     subchild->setIcon(0, KIcon("dialog-ok"));
482                     subchild->setData(0, statusRole, CLIPOK);
483                 }
484             }
485         }
486         else if (child->data(0, statusRole).toInt() == CLIPMISSING) {
487             QString clipPath = searchFileRecursively(searchDir, child->data(0, sizeRole).toString(), child->data(0, hashRole).toString());
488             if (!clipPath.isEmpty()) {
489                 fixed = true;
490                 child->setText(1, clipPath);
491                 child->setIcon(0, KIcon("dialog-ok"));
492                 child->setData(0, statusRole, CLIPOK);
493             }
494         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
495             QString fileName = searchLuma(searchDir, child->data(0, idRole).toString());
496             if (!fileName.isEmpty()) {
497                 fixed = true;
498                 child->setText(1, fileName);
499                 child->setIcon(0, KIcon("dialog-ok"));
500                 child->setData(0, statusRole, LUMAOK);
501             }
502         }
503         else if (child->data(0, typeRole).toInt() == TITLE_IMAGE_ELEMENT && child->data(0, statusRole).toInt() == CLIPPLACEHOLDER) {
504             // Search missing title images
505             QString missingFileName = KUrl(child->text(1)).fileName();
506             QString newPath = searchPathRecursively(searchDir, missingFileName);
507             if (!newPath.isEmpty()) {
508                 // File found
509                 fixed = true;
510                 child->setText(1, newPath);
511                 child->setIcon(0, KIcon("dialog-ok"));
512                 child->setData(0, statusRole, CLIPOK);
513             }
514         }
515         ix++;
516         child = m_ui.treeWidget->topLevelItem(ix);
517     }
518     m_ui.recursiveSearch->setChecked(false);
519     m_ui.recursiveSearch->setEnabled(true);
520     if (fixed) {
521         // original doc was modified
522         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
523         infoXml.setAttribute("modified", "1");
524     }
525     checkStatus();
526 }
527
528
529 QString DocumentChecker::searchLuma(const QDir &dir, const QString &file) const
530 {
531     KUrl searchPath(KdenliveSettings::mltpath());
532     QString fname = KUrl(file).fileName();
533     if (file.contains("PAL"))
534         searchPath.cd("../lumas/PAL");
535     else
536         searchPath.cd("../lumas/NTSC");
537     QString result = searchPath.path(KUrl::AddTrailingSlash) + fname;
538     if (QFile::exists(result))
539         return result;
540     // try to find luma in application path
541     searchPath.clear();
542     searchPath = KUrl(QCoreApplication::applicationDirPath());
543     searchPath.cd("../share/apps/kdenlive/lumas");
544     result = searchPath.path(KUrl::AddTrailingSlash) + fname;
545     if (QFile::exists(result))
546         return result;
547     // Try in Kdenlive's standard KDE path
548     result = KStandardDirs::locate("appdata", "lumas/" + fname);
549     if (!result.isEmpty()) return result;
550     // Try in user's chosen folder 
551     return searchPathRecursively(dir, fname);
552 }
553
554 QString DocumentChecker::searchPathRecursively(const QDir &dir, const QString &fileName) const
555 {
556     QString foundFileName;
557     QStringList filters;
558     filters << fileName;
559     QDir searchDir(dir);
560     searchDir.setNameFilters(filters);
561     QStringList filesAndDirs = searchDir.entryList(QDir::Files | QDir::Readable);
562     if (!filesAndDirs.isEmpty()) return searchDir.absoluteFilePath(filesAndDirs.at(0));
563     searchDir.setNameFilters(QStringList());
564     filesAndDirs = searchDir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
565     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
566         foundFileName = searchPathRecursively(searchDir.absoluteFilePath(filesAndDirs.at(i)), fileName);
567         if (!foundFileName.isEmpty())
568             break;
569     }
570     return foundFileName;
571 }
572
573 QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const
574 {
575     QString foundFileName;
576     QByteArray fileData;
577     QByteArray fileHash;
578     QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable);
579     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
580         QFile file(dir.absoluteFilePath(filesAndDirs.at(i)));
581         if (QString::number(file.size()) == matchSize) {
582             if (file.open(QIODevice::ReadOnly)) {
583                 /*
584                 * 1 MB = 1 second per 450 files (or faster)
585                 * 10 MB = 9 seconds per 450 files (or faster)
586                 */
587                 if (file.size() > 1000000 * 2) {
588                     fileData = file.read(1000000);
589                     if (file.seek(file.size() - 1000000))
590                         fileData.append(file.readAll());
591                 } else
592                     fileData = file.readAll();
593                 file.close();
594                 fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
595                 if (QString(fileHash.toHex()) == matchHash)
596                     return file.fileName();
597             }
598         }
599         //kDebug() << filesAndDirs.at(i) << file.size() << fileHash.toHex();
600     }
601     filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
602     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
603         foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash);
604         if (!foundFileName.isEmpty())
605             break;
606     }
607     return foundFileName;
608 }
609
610 void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int)
611 {
612     int t = item->data(0, typeRole).toInt();
613     if (t == TITLE_FONT_ELEMENT || t == UNKNOWN) return;
614     //|| t == TITLE_IMAGE_ELEMENT) {
615
616     KUrl url = KUrlRequesterDialog::getUrl(item->text(1), m_dialog, i18n("Enter new location for file"));
617     if (url.isEmpty()) return;
618     item->setText(1, url.path());
619     if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0)) {
620         item->setIcon(0, KIcon("dialog-ok"));
621         int id = item->data(0, statusRole).toInt();
622         if (id < 10) item->setData(0, statusRole, CLIPOK);
623         else item->setData(0, statusRole, LUMAOK);
624         checkStatus();
625     } else {
626         item->setIcon(0, KIcon("dialog-close"));
627         int id = item->data(0, statusRole).toInt();
628         if (id < 10) item->setData(0, statusRole, CLIPMISSING);
629         else item->setData(0, statusRole, LUMAMISSING);
630         checkStatus();
631     }
632 }
633
634
635 void DocumentChecker::acceptDialog()
636 {
637     QDomNodeList producers = m_doc.elementsByTagName("producer");
638     QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
639     int ix = 0;
640
641     // prepare transitions
642     QDomNodeList trans = m_doc.elementsByTagName("transition");
643
644     // Mark document as modified
645     m_doc.documentElement().setAttribute("modified", 1);
646
647     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
648     while (child) {
649         if (child->data(0, statusRole).toInt() == SOURCEMISSING) {
650             for (int j = 0; j < child->childCount(); j++) {
651                 fixClipItem(child->child(j), producers, infoproducers, trans);
652             }
653         }
654         else fixClipItem(child, producers, infoproducers, trans);
655         ix++;
656         child = m_ui.treeWidget->topLevelItem(ix);
657     }
658     //QDialog::accept();
659 }
660
661 void DocumentChecker::fixClipItem(QTreeWidgetItem *child, QDomNodeList producers, QDomNodeList infoproducers, QDomNodeList trans)
662 {
663     QDomElement e, property;
664     QDomNodeList properties;
665     int t = child->data(0, typeRole).toInt();
666     if (child->data(0, statusRole).toInt() == CLIPOK) {
667         QString id = child->data(0, idRole).toString();
668         if (t == TITLE_IMAGE_ELEMENT) {
669             // edit images embedded in titles
670             for (int i = 0; i < infoproducers.count(); i++) {
671                 e = infoproducers.item(i).toElement();
672                 if (e.attribute("id") == id) {
673                     // Fix clip
674                     QString xml = e.attribute("xmldata");
675                     xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
676                     e.setAttribute("xmldata", xml);
677                     break;
678                 }
679             }
680             for (int i = 0; i < producers.count(); i++) {
681                 e = producers.item(i).toElement();
682                 if (e.attribute("id").section('_', 0, 0) == id) {
683                     // Fix clip
684                     properties = e.childNodes();
685                     for (int j = 0; j < properties.count(); ++j) {
686                         property = properties.item(j).toElement();
687                         if (property.attribute("name") == "xmldata") {
688                             QString xml = property.firstChild().nodeValue();
689                             xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
690                             property.firstChild().setNodeValue(xml);
691                             break;
692                         }
693                     }
694                 }
695             }
696         } else {
697             // edit clip url
698             for (int i = 0; i < infoproducers.count(); i++) {
699                 e = infoproducers.item(i).toElement();
700                 if (e.attribute("id") == id) {
701                     // Fix clip
702                     e.setAttribute("resource", child->text(1));
703                     e.setAttribute("name", KUrl(child->text(1)).fileName());
704                     e.removeAttribute("_missingsource");
705                     break;
706                 }
707             }
708             for (int i = 0; i < producers.count(); i++) {
709                 e = producers.item(i).toElement();
710                 if (e.attribute("id").section('_', 0, 0) == id || e.attribute("id").section(':', 1, 1) == id) {
711                     // Fix clip
712                     properties = e.childNodes();
713                     for (int j = 0; j < properties.count(); ++j) {
714                         property = properties.item(j).toElement();
715                         if (property.attribute("name") == "resource") {
716                             QString resource = property.firstChild().nodeValue();
717                             if (resource.contains(QRegExp("\\?[0-9]+\\.[0-9]+(&amp;strobe=[0-9]+)?$")))
718                                 property.firstChild().setNodeValue(child->text(1) + '?' + resource.section('?', -1));
719                             else
720                                 property.firstChild().setNodeValue(child->text(1));
721                             break;
722                         }
723                     }
724                 }
725             }
726         }
727     } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER && t != TITLE_FONT_ELEMENT && t != TITLE_IMAGE_ELEMENT) {
728         QString id = child->data(0, idRole).toString();
729         for (int i = 0; i < infoproducers.count(); i++) {
730             e = infoproducers.item(i).toElement();
731             if (e.attribute("id") == id) {
732                 // Fix clip
733                 e.setAttribute("placeholder", '1');
734                 break;
735             }
736         }
737     } else if (child->data(0, statusRole).toInt() == LUMAOK) {
738         for (int i = 0; i < trans.count(); i++) {
739             QString luma = getProperty(trans.at(i).toElement(), "luma");
740             if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
741                 setProperty(trans.at(i).toElement(), "luma", child->text(1));
742                 kDebug() << "replace with; " << child->text(1);
743             }
744         }
745     } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
746         for (int i = 0; i < trans.count(); i++) {
747             QString luma = getProperty(trans.at(i).toElement(), "luma");
748             if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
749                 setProperty(trans.at(i).toElement(), "luma", QString());
750             }
751         }
752     }
753 }
754
755 void DocumentChecker::slotPlaceholders()
756 {
757     int ix = 0;
758     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
759     while (child) {
760         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
761             child->setData(0, statusRole, CLIPPLACEHOLDER);
762             child->setIcon(0, KIcon("dialog-ok"));
763         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
764             child->setData(0, statusRole, LUMAPLACEHOLDER);
765             child->setIcon(0, KIcon("dialog-ok"));
766         }
767         ix++;
768         child = m_ui.treeWidget->topLevelItem(ix);
769     }
770     checkStatus();
771 }
772
773 void DocumentChecker::slotFixDuration()
774 {
775     int ix = 0;
776     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
777     QDomNodeList documentProducers = m_doc.elementsByTagName("producer");
778     while (child) {
779         if (child->data(0, statusRole).toInt() == CLIPWRONGDURATION) {
780             QString id = child->data(0, idRole).toString();
781             bool resetDuration = child->data(0, resetDurationRole).toInt();
782
783             for (int i = 0; i < m_info.count(); i++) {
784                 QDomElement e = m_info.at(i).toElement();
785                 if (e.attribute("id") == id) {
786                     if (m_missingClips.contains(e)) {
787                         // we cannot fix duration of missing clips
788                         resetDuration = false;
789                     }
790                     else {
791                         if (resetDuration) e.removeAttribute("duration");
792                         else e.setAttribute("duration", child->data(0, sizeRole).toString());
793                         child->setData(0, statusRole, CLIPOK);
794                         child->setIcon(0, KIcon("dialog-ok"));
795                     }
796                     break;
797                 }
798             }
799             if (resetDuration) {
800                 // something is wrong in clip durations, so remove them so mlt fetches them again
801                 for (int j = 0; j < documentProducers.count(); j++) {
802                     QDomElement mltProd = documentProducers.at(j).toElement();
803                     QString prodId = mltProd.attribute("id");
804                     if (prodId == id || prodId.startsWith(id + '_')) {
805                         EffectsList::removeProperty(mltProd, "length");
806                     }
807                 }
808             }
809         }
810         ix++;
811         child = m_ui.treeWidget->topLevelItem(ix);
812     }
813     QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
814     infoXml.setAttribute("modified", "1");
815     m_ui.fixDuration->setEnabled(false);
816     checkStatus();
817 }
818
819
820 void DocumentChecker::checkStatus()
821 {
822     bool status = true;
823     int ix = 0;
824     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
825     while (child) {
826         int status = child->data(0, statusRole).toInt();
827         if (status == CLIPMISSING || status == LUMAMISSING || status == CLIPWRONGDURATION) {
828             status = false;
829             break;
830         }
831         ix++;
832         child = m_ui.treeWidget->topLevelItem(ix);
833     }
834     m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status);
835 }
836
837
838 void DocumentChecker::slotDeleteSelected()
839 {
840     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)
841         return;
842     QStringList deletedIds;
843     QStringList deletedLumas;
844     QDomNodeList playlists = m_doc.elementsByTagName("playlist");
845
846     foreach(QTreeWidgetItem *child, m_ui.treeWidget->selectedItems()) {
847         int id = child->data(0, statusRole).toInt();
848         if (id == CLIPMISSING) {
849             deletedIds.append(child->data(0, idRole).toString());
850             delete child;
851         }
852         else if (id == LUMAMISSING) {
853             deletedLumas.append(child->data(0, idRole).toString());
854             delete child;
855         }
856     }
857
858     if (!deletedLumas.isEmpty()) {
859         QDomElement e;
860         QDomNodeList transitions = m_doc.elementsByTagName("transition");
861         foreach (const QString &lumaPath, deletedLumas) {
862             for (int i = 0; i < transitions.count(); i++) {
863                 e = transitions.item(i).toElement();
864                 QString resource = EffectsList::property(e, "luma");
865                 if (resource == lumaPath) EffectsList::removeProperty(e, "luma");
866             }
867         }
868     }
869
870     if (!deletedIds.isEmpty()) {
871         QDomElement e;
872         QDomNodeList producers = m_doc.elementsByTagName("producer");
873         QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
874
875         QDomNode mlt = m_doc.elementsByTagName("mlt").at(0);
876         QDomNode kdenlivedoc = m_doc.elementsByTagName("kdenlivedoc").at(0);
877
878         for (int i = 0, j = 0; i < infoproducers.count() && j < deletedIds.count(); i++) {
879             e = infoproducers.item(i).toElement();
880             if (deletedIds.contains(e.attribute("id"))) {
881                 // Remove clip
882                 kdenlivedoc.removeChild(e);
883                 i--;
884                 j++;
885             }
886         }
887
888         for (int i = 0; i < producers.count(); i++) {
889             e = producers.item(i).toElement();
890             if (deletedIds.contains(e.attribute("id").section('_', 0, 0)) || deletedIds.contains(e.attribute("id").section(':', 1, 1).section('_', 0, 0))) {
891                 // Remove clip
892                 mlt.removeChild(e);
893                 i--;
894             }
895         }
896
897         for (int i = 0; i < playlists.count(); i++) {
898             QDomNodeList entries = playlists.at(i).toElement().elementsByTagName("entry");
899             for (int j = 0; j < entries.count(); j++) {
900                 e = entries.item(j).toElement();
901                 if (deletedIds.contains(e.attribute("producer").section('_', 0, 0)) || deletedIds.contains(e.attribute("producer").section(':', 1, 1).section('_', 0, 0))) {
902                     // Replace clip with blank
903                     while (e.childNodes().count() > 0)
904                         e.removeChild(e.firstChild());
905                     e.setTagName("blank");
906                     e.removeAttribute("producer");
907                     int length = e.attribute("out").toInt() - e.attribute("in").toInt();
908                     e.setAttribute("length", length);
909                     j--;
910                 }
911             }
912         }
913         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
914         infoXml.setAttribute("modified", "1");
915         checkStatus();
916     }
917 }
918
919 void DocumentChecker::checkMissingImagesAndFonts(QStringList images, QStringList fonts, const QString &id, const QString &baseClip)
920 {
921     QDomDocument doc;
922     foreach(const QString &img, images) {
923         if (m_safeImages.contains(img)) continue;
924         if (!KIO::NetAccess::exists(KUrl(img), KIO::NetAccess::SourceSide, 0)) {
925             QDomElement e = doc.createElement("missingclip");
926             e.setAttribute("type", TITLE_IMAGE_ELEMENT);
927             e.setAttribute("resource", img);
928             e.setAttribute("id", id);
929             e.setAttribute("name", baseClip);
930             m_missingClips.append(e);
931         }
932         else m_safeImages.append(img);
933     }
934     foreach(const QString &fontelement, fonts) {
935         if (m_safeFonts.contains(fontelement)) continue;
936         QFont f(fontelement);
937         //kDebug() << "/ / / CHK FONTS: " << fontelement << " = " << QFontInfo(f).family();
938         if (fontelement != QFontInfo(f).family()) {
939             QDomElement e = doc.createElement("missingclip");
940             e.setAttribute("type", TITLE_FONT_ELEMENT);
941             e.setAttribute("resource", fontelement);
942             e.setAttribute("id", id);
943             e.setAttribute("name", baseClip);
944             m_missingClips.append(e);
945         }
946         else m_safeFonts.append(fontelement);
947     }
948 }
949
950
951 void DocumentChecker::slotCheckButtons()
952 {
953     if (m_ui.treeWidget->currentItem()) {
954         QTreeWidgetItem *item = m_ui.treeWidget->currentItem();
955         int t = item->data(0, typeRole).toInt();
956         int s = item->data(0, statusRole).toInt();
957         if (t == TITLE_FONT_ELEMENT || t == TITLE_IMAGE_ELEMENT || s == PROXYMISSING) {
958             m_ui.removeSelected->setEnabled(false);
959         } else m_ui.removeSelected->setEnabled(true);
960     }
961
962 }
963
964 #include "documentchecker.moc"
965
966