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