1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2004-06-15
7  * Description : Albums manager interface - Physical Album helpers.
8  *
9  * Copyright (C) 2006-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
10  * Copyright (C) 2006-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
11  * Copyright (C) 2015      by Mohamed_Anwer <m_dot_anwer at gmx dot com>
12  *
13  * This program is free software; you can redistribute it
14  * and/or modify it under the terms of the GNU General
15  * Public License as published by the Free Software Foundation;
16  * either version 2, or (at your option)
17  * any later version.
18  *
19  * This program is distributed in the hope that it will be useful,
20  * but WITHOUT ANY WARRANTY; without even the implied warranty of
21  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22  * GNU General Public License for more details.
23  *
24  * ============================================================ */
25 
26 #include "albummanager_p.h"
27 
28 namespace Digikam
29 {
30 
scanPAlbums()31 void AlbumManager::scanPAlbums()
32 {
33     d->scanPAlbumsTimer->stop();
34 
35     // first insert all the current normal PAlbums into a map for quick lookup
36 
37     QHash<int, PAlbum*> oldAlbums;
38     AlbumIterator it(d->rootPAlbum);
39 
40     while (it.current())
41     {
42         PAlbum* const a    = (PAlbum*)(*it);
43         oldAlbums[a->id()] = a;
44         ++it;
45     }
46 
47     // scan db and get a list of all albums
48 
49     QList<AlbumInfo> currentAlbums = CoreDbAccess().db()->scanAlbums();
50 
51     // sort by relative path so that parents are created before children
52 
53     std::sort(currentAlbums.begin(), currentAlbums.end());
54 
55     QList<AlbumInfo> newAlbums;
56 
57     // go through all the Albums and see which ones are already present
58 
59     foreach (const AlbumInfo& info, currentAlbums)
60     {
61         // check that location of album is available
62 
63         if (d->showOnlyAvailableAlbums &&
64             !CollectionManager::instance()->locationForAlbumRootId(info.albumRootId).isAvailable())
65         {
66             continue;
67         }
68 
69         if (oldAlbums.contains(info.id))
70         {
71             oldAlbums.remove(info.id);
72         }
73         else
74         {
75             newAlbums << info;
76         }
77     }
78 
79     // now oldAlbums contains all the deleted albums and
80     // newAlbums contains all the new albums
81 
82     // delete old albums, informing all frontends
83 
84     // The albums have to be removed with children being removed first,
85     // removePAlbum takes care of that.
86     // So we only feed it the albums from oldAlbums topmost in hierarchy.
87 
88     QSet<PAlbum*> topMostOldAlbums;
89 
90     foreach (PAlbum* const album, oldAlbums)
91     {
92         if (album->isTrashAlbum())
93         {
94             continue;
95         }
96 
97         if (!album->parent() || !oldAlbums.contains(album->parent()->id()))
98         {
99             topMostOldAlbums << album;
100         }
101     }
102 
103     foreach (PAlbum* const album, topMostOldAlbums)
104     {
105         // recursively removes all children and the album
106 
107         removePAlbum(album);
108     }
109 
110     // sort by relative path so that parents are created before children
111 
112     std::sort(newAlbums.begin(), newAlbums.end());
113 
114     // create all new albums
115 
116     foreach (const AlbumInfo& info, newAlbums)
117     {
118         if (info.relativePath.isEmpty())
119         {
120             continue;
121         }
122 
123         PAlbum* album  = nullptr;
124         PAlbum* parent = nullptr;
125 
126         if (info.relativePath == QLatin1String("/"))
127         {
128             // Albums that represent the root directory of an album root
129             // We have them as here new albums first time after their creation
130 
131             parent = d->rootPAlbum;
132             album  = d->albumRootAlbumHash.value(info.albumRootId);
133 
134             if (!album)
135             {
136                 qCDebug(DIGIKAM_GENERAL_LOG) << "Did not find album root album in hash";
137                 continue;
138             }
139 
140             // it has been created from the collection location
141             // with album root id, parentPath "/" and a name, but no album id yet.
142 
143             album->m_id = info.id;
144         }
145         else
146         {
147             // last section, no slash
148 
149             QString name = info.relativePath.section(QLatin1Char('/'), -1, -1);
150 
151             // all but last sections, leading slash, no trailing slash
152 
153             QString parentPath = info.relativePath.section(QLatin1Char('/'), 0, -2);
154 
155             if (parentPath.isEmpty())
156             {
157                 parent = d->albumRootAlbumHash.value(info.albumRootId);
158             }
159             else
160             {
161                 parent = d->albumPathHash.value(PAlbumPath(info.albumRootId, parentPath));
162             }
163 
164             if (!parent)
165             {
166                 qCDebug(DIGIKAM_GENERAL_LOG) << "Could not find parent with url: "
167                                              << parentPath << " for: "
168                                              << info.relativePath;
169                 continue;
170             }
171 
172             // Create the new album
173 
174             album = new PAlbum(info.albumRootId, parentPath, name, info.id);
175         }
176 
177         album->m_caption  = info.caption;
178         album->m_category = info.category;
179         album->m_date     = info.date;
180         album->m_iconId   = info.iconId;
181 
182         insertPAlbum(album, parent);
183 
184         if (album->isAlbumRoot())
185         {
186             // Inserting virtual Trash PAlbum for AlbumsRootAlbum using special constructor
187 
188             PAlbum* trashAlbum = new PAlbum(album->title(), album->id());
189             insertPAlbum(trashAlbum, album);
190         }
191     }
192 
193     if (!topMostOldAlbums.isEmpty() || !newAlbums.isEmpty())
194     {
195         emit signalAlbumsUpdated(Album::PHYSICAL);
196     }
197 
198     getAlbumItemsCount();
199 }
200 
updateChangedPAlbums()201 void AlbumManager::updateChangedPAlbums()
202 {
203     d->updatePAlbumsTimer->stop();
204 
205     // scan db and get a list of all albums
206 
207     QList<AlbumInfo> currentAlbums = CoreDbAccess().db()->scanAlbums();
208     bool needScanPAlbums           = false;
209 
210     // Find the AlbumInfo for each id in changedPAlbums
211 
212     foreach (int id, d->changedPAlbums)
213     {
214         foreach (const AlbumInfo& info, currentAlbums)
215         {
216             if (info.id == id)
217             {
218                 d->changedPAlbums.remove(info.id);
219 
220                 PAlbum* album = findPAlbum(info.id);
221 
222                 if (album)
223                 {
224                     // Renamed?
225 
226                     if (info.relativePath != QLatin1String("/"))
227                     {
228                         // Handle rename of album name
229                         // last section, no slash
230 
231                         QString name       = info.relativePath.section(QLatin1Char('/'), -1, -1);
232                         QString parentPath = info.relativePath;
233                         parentPath.chop(name.length());
234 
235                         if (parentPath != album->m_parentPath || info.albumRootId != album->albumRootId())
236                         {
237                             // Handle actual move operations: trigger ScanPAlbums
238 
239                             needScanPAlbums = true;
240                             removePAlbum(album);
241                             break;
242                         }
243                         else if (name != album->title())
244                         {
245                             album->setTitle(name);
246                             updateAlbumPathHash();
247                             emit signalAlbumRenamed(album);
248                         }
249                     }
250 
251                     // Update caption, collection, date
252 
253                     album->m_caption  = info.caption;
254                     album->m_category = info.category;
255                     album->m_date     = info.date;
256 
257                     // Icon changed?
258 
259                     if (album->m_iconId != info.iconId)
260                     {
261                         album->m_iconId = info.iconId;
262                         emit signalAlbumIconChanged(album);
263                     }
264                 }
265             }
266         }
267     }
268 
269     if (needScanPAlbums)
270     {
271         scanPAlbums();
272     }
273 }
274 
allPAlbums() const275 AlbumList AlbumManager::allPAlbums() const
276 {
277     AlbumList list;
278 
279     if (d->rootPAlbum)
280     {
281         list.append(d->rootPAlbum);
282     }
283 
284     AlbumIterator it(d->rootPAlbum);
285 
286     while (it.current())
287     {
288         list.append(*it);
289         ++it;
290     }
291 
292     return list;
293 }
294 
currentPAlbum() const295 PAlbum* AlbumManager::currentPAlbum() const
296 {
297     /**
298      * Temporary fix, to return multiple items,
299      * iterate and cast each element
300      */
301     if (!d->currentAlbums.isEmpty())
302     {
303         return dynamic_cast<PAlbum*>(d->currentAlbums.first());
304     }
305     else
306     {
307         return nullptr;
308     }
309 }
310 
findPAlbum(const QUrl & url) const311 PAlbum* AlbumManager::findPAlbum(const QUrl& url) const
312 {
313     CollectionLocation location = CollectionManager::instance()->locationForUrl(url);
314 
315     if (location.isNull())
316     {
317         return nullptr;
318     }
319 
320     return d->albumPathHash.value(PAlbumPath(location.id(), CollectionManager::instance()->album(location, url)));
321 }
322 
findPAlbum(int id) const323 PAlbum* AlbumManager::findPAlbum(int id) const
324 {
325     if (!d->rootPAlbum)
326     {
327         return nullptr;
328     }
329 
330     int gid = d->rootPAlbum->globalID() + id;
331 
332     return static_cast<PAlbum*>((d->allAlbumsIdHash.value(gid)));
333 }
334 
335 
createPAlbum(const QString & albumRootPath,const QString & name,const QString & caption,const QDate & date,const QString & category,QString & errMsg)336 PAlbum* AlbumManager::createPAlbum(const QString& albumRootPath, const QString& name,
337                                    const QString& caption, const QDate& date,
338                                    const QString& category,
339                                    QString& errMsg)
340 {
341     CollectionLocation location = CollectionManager::instance()->locationForAlbumRootPath(albumRootPath);
342 
343     return createPAlbum(location, name, caption, date, category, errMsg);
344 }
345 
createPAlbum(const CollectionLocation & location,const QString & name,const QString & caption,const QDate & date,const QString & category,QString & errMsg)346 PAlbum* AlbumManager::createPAlbum(const CollectionLocation& location, const QString& name,
347                                    const QString& caption, const QDate& date,
348                                    const QString& category,
349                                    QString& errMsg)
350 {
351     if (location.isNull() || !location.isAvailable())
352     {
353         errMsg = i18n("The collection location supplied is invalid or currently not available.");
354         return nullptr;
355     }
356 
357     PAlbum* const album = d->albumRootAlbumHash.value(location.id());
358 
359     if (!album)
360     {
361         errMsg = i18n("No album for collection location: Internal error");
362         return nullptr;
363     }
364 
365     return createPAlbum(album, name, caption, date, category, errMsg);
366 }
367 
createPAlbum(PAlbum * parent,const QString & name,const QString & caption,const QDate & date,const QString & category,QString & errMsg)368 PAlbum* AlbumManager::createPAlbum(PAlbum*        parent,
369                                    const QString& name,
370                                    const QString& caption,
371                                    const QDate&   date,
372                                    const QString& category,
373                                    QString&       errMsg)
374 {
375     if (!parent)
376     {
377         errMsg = i18n("No parent found for album.");
378         return nullptr;
379     }
380 
381     // sanity checks
382 
383     if (name.isEmpty())
384     {
385         errMsg = i18n("Album name cannot be empty.");
386         return nullptr;
387     }
388 
389     if (name.contains(QLatin1Char('/')))
390     {
391         errMsg = i18n("Album name cannot contain '/'.");
392         return nullptr;
393     }
394 
395     if (parent->isRoot())
396     {
397         errMsg = i18n("createPAlbum does not accept the root album as parent.");
398         return nullptr;
399     }
400 
401     QString albumPath = parent->isAlbumRoot() ? QString(QLatin1Char('/') + name) : QString(parent->albumPath() + QLatin1Char('/') + name);
402     int albumRootId   = parent->albumRootId();
403 
404     // first check if we have a sibling album with the same name
405 
406     PAlbum* child = static_cast<PAlbum*>(parent->firstChild());
407 
408     while (child)
409     {
410         if ((child->albumRootId() == albumRootId) && (child->albumPath() == albumPath))
411         {
412             errMsg = i18n("An existing album has the same name.");
413             return nullptr;
414         }
415 
416         child = static_cast<PAlbum*>(child->next());
417     }
418 
419     CoreDbUrl url   = parent->databaseUrl();
420     url             = url.adjusted(QUrl::StripTrailingSlash);
421     url.setPath(url.path() + QLatin1Char('/') + name);
422     QUrl fileUrl    = url.fileUrl();
423 
424     bool ret        = QDir().mkpath(fileUrl.toLocalFile());
425 
426     if (!ret)
427     {
428         errMsg = i18n("Failed to create directory '%1'", fileUrl.toString()); // TODO add tags?
429         return nullptr;
430     }
431 
432     ChangingDB changing(d);
433     int        id = CoreDbAccess().db()->addAlbum(albumRootId, albumPath, caption, date, category);
434 
435     if (id == -1)
436     {
437         errMsg = i18n("Failed to add album to database");
438         return nullptr;
439     }
440 
441     QString parentPath;
442 
443     if (!parent->isAlbumRoot())
444     {
445         parentPath = parent->albumPath();
446     }
447 
448     PAlbum* const album = new PAlbum(albumRootId, parentPath, name, id);
449     album->m_caption    = caption;
450     album->m_category   = category;
451     album->m_date       = date;
452 
453     insertPAlbum(album, parent);
454     emit signalAlbumsUpdated(Album::PHYSICAL);
455 
456     return album;
457 }
458 
renamePAlbum(PAlbum * album,const QString & newName,QString & errMsg)459 bool AlbumManager::renamePAlbum(PAlbum* album, const QString& newName,
460                                 QString& errMsg)
461 {
462     if (!album)
463     {
464         errMsg = i18n("No such album");
465         return false;
466     }
467 
468     if (album == d->rootPAlbum)
469     {
470         errMsg = i18n("Cannot rename root album");
471         return false;
472     }
473 
474     if (album->isAlbumRoot())
475     {
476         errMsg = i18n("Cannot rename album root album");
477         return false;
478     }
479 
480     if (newName.contains(QLatin1Char('/')))
481     {
482         errMsg = i18n("Album name cannot contain '/'");
483         return false;
484     }
485 
486     // first check if we have another sibling with the same name
487 
488     if (hasDirectChildAlbumWithTitle(album->m_parent, newName))
489     {
490         errMsg = i18n("Another album with the same name already exists.\n"
491                       "Please choose another name.");
492         return false;
493     }
494 
495     d->albumWatch->removeWatchedPAlbums(album);
496 
497     // We use a private shortcut around collection scanner noticing our changes,
498     // we rename them directly. Faster.
499 
500     ScanController::instance()->suspendCollectionScan();
501 
502     QDir dir(album->albumRootPath() + album->m_parentPath);
503     bool ret = dir.rename(album->title(), newName);
504 
505     if (!ret)
506     {
507         ScanController::instance()->resumeCollectionScan();
508 
509         errMsg = i18n("Failed to rename Album");
510         return false;
511     }
512 
513     QString oldAlbumPath = album->albumPath();
514     album->setTitle(newName);
515     album->m_path        = newName;
516     QString newAlbumPath = album->albumPath();
517 
518     // now rename the album and subalbums in the database
519     {
520         CoreDbAccess access;
521         ChangingDB changing(d);
522         access.db()->renameAlbum(album->id(), album->albumRootId(), album->albumPath());
523 
524         PAlbum* subAlbum = nullptr;
525         AlbumIterator it(album);
526 
527         while ((subAlbum = static_cast<PAlbum*>(it.current())) != nullptr)
528         {
529             subAlbum->m_parentPath = newAlbumPath + subAlbum->m_parentPath.mid(oldAlbumPath.length());
530             access.db()->renameAlbum(subAlbum->id(), album->albumRootId(), subAlbum->albumPath());
531             emit signalAlbumNewPath(subAlbum);
532             ++it;
533         }
534     }
535 
536     updateAlbumPathHash();
537     emit signalAlbumRenamed(album);
538 
539     ScanController::instance()->resumeCollectionScan();
540 
541     return true;
542 }
543 
updatePAlbumIcon(PAlbum * album,qlonglong iconID,QString & errMsg)544 bool AlbumManager::updatePAlbumIcon(PAlbum* album, qlonglong iconID, QString& errMsg)
545 {
546     if (!album)
547     {
548         errMsg = i18n("No such album");
549         return false;
550     }
551 
552     if (album == d->rootPAlbum)
553     {
554         errMsg = i18n("Cannot edit root album");
555         return false;
556     }
557 
558     {
559         CoreDbAccess access;
560         ChangingDB changing(d);
561         access.db()->setAlbumIcon(album->id(), iconID);
562         album->m_iconId = iconID;
563     }
564 
565     emit signalAlbumIconChanged(album);
566 
567     return true;
568 }
569 
getPAlbumsCount() const570 QMap<int, int> AlbumManager::getPAlbumsCount() const
571 {
572     return d->pAlbumsCount;
573 }
574 
insertPAlbum(PAlbum * album,PAlbum * parent)575 void AlbumManager::insertPAlbum(PAlbum* album, PAlbum* parent)
576 {
577     if (!album)
578     {
579         return;
580     }
581 
582     emit signalAlbumAboutToBeAdded(album, parent, parent ? parent->lastChild() : nullptr);
583 
584     if (parent)
585     {
586         album->setParent(parent);
587     }
588 
589     d->albumPathHash[PAlbumPath(album)]   = album;
590     d->allAlbumsIdHash[album->globalID()] = album;
591 
592     emit signalAlbumAdded(album);
593 }
594 
removePAlbum(PAlbum * album)595 void AlbumManager::removePAlbum(PAlbum* album)
596 {
597     if (!album)
598     {
599         return;
600     }
601 
602     // remove all children of this album
603 
604     Album* child        = album->firstChild();
605     PAlbum* toBeRemoved = nullptr;
606 
607     while (child)
608     {
609         Album* const next = child->next();
610         toBeRemoved       = dynamic_cast<PAlbum*>(child);
611 
612         if (toBeRemoved)
613         {
614             removePAlbum(toBeRemoved);
615             toBeRemoved = nullptr;
616         }
617 
618         child             = next;
619     }
620 
621     emit signalAlbumAboutToBeDeleted(album);
622     d->albumPathHash.remove(PAlbumPath(album));
623     d->allAlbumsIdHash.remove(album->globalID());
624 
625     CoreDbUrl url = album->databaseUrl();
626 
627     if (!d->currentAlbums.isEmpty())
628     {
629         if (album == d->currentAlbums.first())
630         {
631             d->currentAlbums.clear();
632             emit signalAlbumCurrentChanged(d->currentAlbums);
633         }
634     }
635 
636     if (album->isAlbumRoot())
637     {
638         d->albumRootAlbumHash.remove(album->albumRootId());
639     }
640 
641     emit signalAlbumDeleted(album);
642     quintptr deletedAlbum = reinterpret_cast<quintptr>(album);
643     delete album;
644 
645     emit signalAlbumHasBeenDeleted(deletedAlbum);
646 }
647 
removeWatchedPAlbums(const PAlbum * const album)648 void AlbumManager::removeWatchedPAlbums(const PAlbum* const album)
649 {
650     d->albumWatch->removeWatchedPAlbums(album);
651 }
652 
653 } // namespace Digikam
654