]> git.sesse.net Git - kdenlive/blob - src/archivewidget.cpp
Archiving slideshows now works
[kdenlive] / src / archivewidget.cpp
1 /***************************************************************************
2  *   Copyright (C) 2011 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 "archivewidget.h"
22 #include "titlewidget.h"
23
24 #include <KLocale>
25 #include <KDiskFreeSpaceInfo>
26 #include <KUrlRequester>
27 #include <KFileDialog>
28 #include <KMessageBox>
29 #include <KApplication>
30 #include <KIO/NetAccess>
31
32 #include <KDebug>
33 #include <QTreeWidget>
34 #include "projectsettings.h"
35
36
37 ArchiveWidget::ArchiveWidget(QDomDocument doc, QList <DocClipBase*> list, QStringList luma_list, QWidget * parent) :
38         QDialog(parent),
39         m_requestedSize(0),
40         m_copyJob(NULL),
41         m_doc(doc)
42 {
43     setupUi(this);
44     setWindowTitle(i18n("Archive Project"));
45     archive_url->setUrl(KUrl(QDir::homePath()));
46     connect(archive_url, SIGNAL(textChanged (const QString &)), this, SLOT(slotCheckSpace()));
47
48     // Setup categories
49     QTreeWidgetItem *videos = new QTreeWidgetItem(files_list, QStringList() << i18n("Video clips"));
50     videos->setIcon(0, KIcon("video-x-generic"));
51     videos->setData(0, Qt::UserRole, "videos");
52     videos->setExpanded(false);
53     QTreeWidgetItem *sounds = new QTreeWidgetItem(files_list, QStringList() << i18n("Audio clips"));
54     sounds->setIcon(0, KIcon("audio-x-generic"));
55     sounds->setData(0, Qt::UserRole, "sounds");
56     sounds->setExpanded(false);
57     QTreeWidgetItem *images = new QTreeWidgetItem(files_list, QStringList() << i18n("Image clips"));
58     images->setIcon(0, KIcon("image-x-generic"));
59     images->setData(0, Qt::UserRole, "images");
60     images->setExpanded(false);
61     QTreeWidgetItem *slideshows = new QTreeWidgetItem(files_list, QStringList() << i18n("Slideshow clips"));
62     slideshows->setIcon(0, KIcon("image-x-generic"));
63     slideshows->setData(0, Qt::UserRole, "slideshows");
64     slideshows->setExpanded(false);
65     QTreeWidgetItem *texts = new QTreeWidgetItem(files_list, QStringList() << i18n("Text clips"));
66     texts->setIcon(0, KIcon("text-plain"));
67     texts->setData(0, Qt::UserRole, "texts");
68     texts->setExpanded(false);
69     QTreeWidgetItem *others = new QTreeWidgetItem(files_list, QStringList() << i18n("Other clips"));
70     others->setIcon(0, KIcon("unknown"));
71     others->setData(0, Qt::UserRole, "others");
72     others->setExpanded(false);
73     QTreeWidgetItem *lumas = new QTreeWidgetItem(files_list, QStringList() << i18n("Luma files"));
74     lumas->setIcon(0, KIcon("image-x-generic"));
75     lumas->setData(0, Qt::UserRole, "lumas");
76     lumas->setExpanded(false);
77     
78     QTreeWidgetItem *proxies = new QTreeWidgetItem(files_list, QStringList() << i18n("Proxy clips"));
79     proxies->setIcon(0, KIcon("video-x-generic"));
80     proxies->setData(0, Qt::UserRole, "proxy");
81     proxies->setExpanded(false);
82     
83     // process all files
84     QStringList allFonts;
85     KUrl::List fileUrls;
86     QStringList fileNames;
87     generateItems(lumas, luma_list);
88
89     QStringList slideUrls;
90     QStringList audioUrls;
91     QStringList videoUrls;
92     QStringList imageUrls;
93     QStringList otherUrls;
94     QStringList proxyUrls;
95
96     for (int i = 0; i < list.count(); i++) {
97         DocClipBase *clip = list.at(i);
98         CLIPTYPE t = clip->clipType();
99         if (t == SLIDESHOW) {
100             KUrl slideUrl = clip->fileURL();
101             //TODO: Slideshow files
102             slideUrls << slideUrl.path();
103         }
104         else if (t == IMAGE) imageUrls << clip->fileURL().path();
105         else if (t == TEXT) {
106             QStringList imagefiles = TitleWidget::extractImageList(clip->getProperty("xmldata"));
107             QStringList fonts = TitleWidget::extractFontList(clip->getProperty("xmldata"));
108             imageUrls << imagefiles;
109             allFonts << fonts;
110         } else if (t == PLAYLIST) {
111             QStringList files = ProjectSettings::extractPlaylistUrls(clip->fileURL().path());
112             otherUrls << files;
113         }
114         else if (!clip->fileURL().isEmpty()) {
115             if (t == AUDIO) audioUrls << clip->fileURL().path();
116             else {
117                 videoUrls << clip->fileURL().path();
118                 // Check if we have a proxy
119                 QString proxy = clip->getProperty("proxy");
120                 if (!proxy.isEmpty() && proxy != "-" && QFile::exists(proxy)) proxyUrls << proxy;
121             }
122         }
123     }
124
125     generateItems(sounds, audioUrls);
126     generateItems(videos, videoUrls);
127     generateItems(images, imageUrls);
128     generateItems(slideshows, slideUrls);
129     generateItems(others, otherUrls);
130     generateItems(proxies, proxyUrls);
131     
132 #if QT_VERSION >= 0x040500
133     allFonts.removeDuplicates();
134 #endif
135
136     //TODO: fonts
137
138     // Hide unused categories, add item count
139     int total = 0;
140     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
141         int items = files_list->topLevelItem(i)->childCount();
142         if (items == 0) {
143             files_list->topLevelItem(i)->setHidden(true);
144         }
145         else {
146             total += items;
147             files_list->topLevelItem(i)->setText(0, files_list->topLevelItem(i)->text(0) + " " + i18np("(%1 item)", "(%1 items)", items));
148         }
149     }
150     
151     project_files->setText(i18n("%1 files to archive, requires %2", total, KIO::convertSize(m_requestedSize)));
152     buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
153     connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(slotStartArchiving()));
154     buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
155     
156     slotCheckSpace();
157 }
158
159 ArchiveWidget::~ArchiveWidget()
160 {
161 }
162
163 void ArchiveWidget::generateItems(QTreeWidgetItem *parentItem, QStringList items)
164 {
165     QStringList filesList;
166     QString fileName;
167     int ix = 0;
168     bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == "slideshows";
169     foreach(const QString & file, items) {
170         QTreeWidgetItem *item = new QTreeWidgetItem(parentItem, QStringList() << file);
171         fileName = KUrl(file).fileName();
172         if (isSlideshow) {
173             // we store each slideshow in a separate subdirectory
174             item->setData(0, Qt::UserRole, ix);
175             ix++;
176             KUrl slideUrl(file);
177             QDir dir(slideUrl.directory(KUrl::AppendTrailingSlash));
178             if (slideUrl.fileName().startsWith(".all.")) {
179                 // mimetype slideshow (for example *.png)
180                     QStringList filters;
181                     QString extension;
182                     // TODO: improve jpeg image detection with extension like jpeg, requires change in MLT image producers
183                     filters << "*." + slideUrl.fileName().section('.', -1);
184                     dir.setNameFilters(filters);
185                     QFileInfoList resultList = dir.entryInfoList(QDir::Files);
186                     QStringList slideImages;
187                     for (int i = 0; i < resultList.count(); i++) {
188                         m_requestedSize += resultList.at(i).size();
189                         slideImages << resultList.at(i).absoluteFilePath();
190                     }
191                     item->setData(0, Qt::UserRole + 1, slideImages);
192             }
193             else {
194                 // pattern url (like clip%.3d.png)
195                 QStringList result = dir.entryList(QDir::Files);
196                 QString filter = slideUrl.fileName();
197                 QString ext = filter.section('.', -1);
198                 filter = filter.section('%', 0, -2);
199                 QString regexp = "^" + filter + "\\d+\\." + ext + "$";
200                 QRegExp rx(regexp);
201                 QStringList slideImages;
202                 QString directory = dir.absolutePath();
203                 if (!directory.endsWith('/')) directory.append('/');
204                 foreach(const QString & path, result) {
205                     if (rx.exactMatch(path)) {
206                         m_requestedSize += QFileInfo(directory + path).size();
207                         slideImages <<  directory + path;
208                     }
209                 }
210                 item->setData(0, Qt::UserRole + 1, slideImages);
211             }                    
212         }
213         else if (filesList.contains(fileName)) {
214             // we have 2 files with same name
215             int ix = 0;
216             QString newFileName = fileName.section('.', 0, -2) + "_" + QString::number(ix) + "." + fileName.section('.', -1);
217             while (filesList.contains(newFileName)) {
218                 ix ++;
219                 newFileName = fileName.section('.', 0, -2) + "_" + QString::number(ix) + "." + fileName.section('.', -1);
220             }
221             fileName = newFileName;
222             item->setData(0, Qt::UserRole, fileName);
223         }
224         if (!isSlideshow) {
225             m_requestedSize += QFileInfo(file).size();
226             filesList << fileName;
227         }
228     }
229 }
230
231 void ArchiveWidget::slotCheckSpace()
232 {
233     KDiskFreeSpaceInfo inf = KDiskFreeSpaceInfo::freeSpaceInfo( archive_url->url().path());
234     KIO::filesize_t freeSize = inf.available();;
235     if (freeSize > m_requestedSize) {
236         // everything is ok
237         buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
238         icon_info->setPixmap(KIcon("dialog-ok").pixmap(16, 16));
239         text_info->setText(i18n("Available space on drive: %1", KIO::convertSize(freeSize)));
240     }
241     else {
242         buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
243         icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
244         text_info->setText(i18n("Not enough space on drive, free space: %1", KIO::convertSize(freeSize)));
245     }
246 }
247
248 bool ArchiveWidget::slotStartArchiving(bool firstPass)
249 {
250     if (firstPass && m_copyJob) {
251         // archiving in progress, abort
252         m_copyJob->kill(KJob::EmitResult);
253         return true;
254     }
255     if (!firstPass) m_copyJob = NULL;
256     else {
257         //starting archiving
258         m_duplicateFiles.clear();
259         m_replacementList.clear();
260     }
261     KUrl::List files;
262     KUrl destUrl;
263     QTreeWidgetItem *parentItem;
264     bool isSlideshow = false;
265     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
266         parentItem = files_list->topLevelItem(i);
267         if (parentItem->data(0, Qt::UserRole).toString() == "slideshows") {
268             KIO::NetAccess::mkdir(KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + "slideshows"), this);
269             isSlideshow = true;
270         }
271         if (parentItem->childCount() > 0 && !parentItem->isDisabled()) {
272             files_list->setCurrentItem(parentItem);
273             if (!isSlideshow) parentItem->setDisabled(true);
274             destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + parentItem->data(0, Qt::UserRole).toString());
275             QTreeWidgetItem *item;
276             for (int j = 0; j < parentItem->childCount(); j++) {
277                 item = parentItem->child(j);
278                 // Special case: slideshows
279                 if (isSlideshow) {
280                     if (item->isDisabled()) {
281                         continue;
282                     }
283                     destUrl = KUrl(destUrl.path(KUrl::AddTrailingSlash) + item->data(0, Qt::UserRole).toString() + "/");
284                     QStringList srcFiles = item->data(0, Qt::UserRole + 1).toStringList();
285                     for (int k = 0; k < srcFiles.count(); k++) {
286                         files << KUrl(srcFiles.at(k));
287                     }
288                     item->setDisabled(true);
289                     if (parentItem->indexOfChild(item) == parentItem->childCount() - 1) {
290                         // We have processed all slideshows
291                         parentItem->setDisabled(true);
292                     }
293                     break;
294                 }
295                 else if (item->data(0, Qt::UserRole).isNull()) {
296                     files << KUrl(item->text(0));
297                 }
298                 else {
299                     // We must rename the destination file, since another file with same name exists
300                     //TODO: monitor progress
301                     m_duplicateFiles.insert(KUrl(item->text(0)), KUrl(destUrl.path(KUrl::AddTrailingSlash) + item->data(0, Qt::UserRole).toString()));
302                 }
303             }
304             break;
305         }
306     }
307
308     if (destUrl.isEmpty()) {
309         if (m_duplicateFiles.isEmpty()) return false;        
310         QMapIterator<KUrl, KUrl> i(m_duplicateFiles);
311         KUrl startJob;
312         if (i.hasNext()) {
313             i.next();
314             startJob = i.key();
315             KIO::CopyJob *job = KIO::copyAs(startJob, i.value(), KIO::HideProgressInfo);
316             connect(job, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *)));
317             connect(job, SIGNAL(processedSize(KJob *, qulonglong)), this, SLOT(slotArchivingProgress(KJob *, qulonglong)));
318             m_duplicateFiles.remove(startJob);
319         }
320         return true;
321     }
322
323     if (files.isEmpty()) slotStartArchiving(false);
324
325     KIO::NetAccess::mkdir(destUrl, this);
326     m_copyJob = KIO::copy (files, destUrl, KIO::HideProgressInfo);
327     connect(m_copyJob, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *)));
328     connect(m_copyJob, SIGNAL(processedSize(KJob *, qulonglong)), this, SLOT(slotArchivingProgress(KJob *, qulonglong)));
329
330     if (firstPass) {
331         progressBar->setValue(0);
332         buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Abort"));
333     }
334     return true;
335 }
336
337 void ArchiveWidget::slotArchivingFinished(KJob *job)
338 {
339     if (job->error() == 0) {
340         if (slotStartArchiving(false)) {
341             // We still have files to archive
342             return;
343         }
344         else {
345             // Archiving finished
346             progressBar->setValue(100);
347             if (processProjectFile()) {
348                 icon_info->setPixmap(KIcon("dialog-ok").pixmap(16, 16));
349                 text_info->setText(i18n("Project was successfully archived."));
350             }
351             else {
352                 icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
353                 text_info->setText(i18n("There was an error processing project file"));
354             }
355         }
356     }
357     else {
358         m_copyJob = NULL;
359         icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
360         text_info->setText(i18n("There was an error while copying the files: %1", job->errorString()));
361     }
362     buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
363     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
364         files_list->topLevelItem(i)->setDisabled(false);
365     }
366 }
367
368 void ArchiveWidget::slotArchivingProgress(KJob *, qulonglong size)
369 {
370     progressBar->setValue((int) 100 * size / m_requestedSize);
371 }
372
373
374 bool ArchiveWidget::processProjectFile()
375 {
376     KUrl destUrl;
377     QTreeWidgetItem *item;
378     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
379         QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
380         if (parentItem->childCount() > 0) {
381             destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + parentItem->data(0, Qt::UserRole).toString());
382             bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == "slideshows";
383             for (int j = 0; j < parentItem->childCount(); j++) {
384                 item = parentItem->child(j);
385                 KUrl src(item->text(0));
386                 KUrl dest = destUrl;
387                 if (isSlideshow) {
388                     dest = KUrl(destUrl.path(KUrl::AddTrailingSlash) + item->data(0, Qt::UserRole).toString() + "/" + src.fileName());
389                 }
390                 else if (item->data(0, Qt::UserRole).isNull()) {
391                     dest.addPath(src.fileName());
392                 }
393                 else {
394                     dest.addPath(item->data(0, Qt::UserRole).toString());
395                 }
396                 m_replacementList.insert(src, dest);
397                 kDebug()<<"___ PROCESS ITEM :"<<src << "="<<dest;
398             }
399         }
400     }
401     
402     // process kdenlive producers           
403     QDomElement mlt = m_doc.documentElement();
404     QString root = mlt.attribute("root") + "/";
405     
406     QDomNodeList prods = mlt.elementsByTagName("kdenlive_producer");
407     for (int i = 0; i < prods.count(); i++) {
408         QDomElement e = prods.item(i).toElement();
409         if (e.isNull()) continue;
410         if (e.hasAttribute("resource")) {
411             KUrl src(e.attribute("resource"));
412             KUrl dest = m_replacementList.value(src);
413             if (!dest.isEmpty()) e.setAttribute("resource", dest.path());
414         }
415         if (e.hasAttribute("proxy") && e.attribute("proxy") != "-") {
416             KUrl src(e.attribute("proxy"));
417             KUrl dest = m_replacementList.value(src);
418             if (!dest.isEmpty()) e.setAttribute("proxy", dest.path());
419         }
420     }
421
422     // process mlt producers
423     prods = mlt.elementsByTagName("producer");
424     for (int i = 0; i < prods.count(); i++) {
425         QDomElement e = prods.item(i).toElement();
426         if (e.isNull()) continue;
427         QString src = EffectsList::property(e, "resource");
428         if (!src.isEmpty()) {
429             if (!src.startsWith('/')) src.prepend(root);
430             KUrl srcUrl(src);
431             KUrl dest = m_replacementList.value(src);
432             if (!dest.isEmpty()) EffectsList::setProperty(e, "resource", dest.path());
433         }
434     }
435
436     // process mlt transitions (for luma files)
437     prods = mlt.elementsByTagName("transition");
438     QString attribute;
439     for (int i = 0; i < prods.count(); i++) {
440         QDomElement e = prods.item(i).toElement();
441         if (e.isNull()) continue;
442         attribute = "resource";
443         QString src = EffectsList::property(e, attribute);
444         if (src.isEmpty()) attribute = "luma";
445         src = EffectsList::property(e, attribute);
446         if (!src.isEmpty()) {
447             if (!src.startsWith('/')) src.prepend(root);
448             KUrl srcUrl(src);
449             KUrl dest = m_replacementList.value(src);
450             if (!dest.isEmpty()) EffectsList::setProperty(e, attribute, dest.path());
451         }
452     }
453
454     QString path = archive_url->url().path(KUrl::AddTrailingSlash) + "project.kdenlive";
455     QFile file(path);
456     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
457         kWarning() << "//////  ERROR writing to file: " << path;
458         KMessageBox::error(kapp->activeWindow(), i18n("Cannot write to file %1", path));
459         return false;
460     }
461
462     file.write(m_doc.toString().toUtf8());
463     if (file.error() != QFile::NoError) {
464         KMessageBox::error(kapp->activeWindow(), i18n("Cannot write to file %1", path));
465         file.close();
466         return false;
467     }
468     file.close();
469     return true;
470 }
471