MythTV master
imagescanner.cpp
Go to the documentation of this file.
1#include "imagescanner.h"
2
3#include <thread>
4
5#include "libmythbase/mythcorecontext.h" // for gCoreContext
7
8#include "imagemetadata.h"
9
15template <class DBFS>
17 : MThread("ImageScanner"),
18 m_dbfs(*dbfs),
19 m_thumb(*thumbGen),
20 m_dir(m_dbfs.GetImageFilters())
21{ }
22
23
24template <class DBFS>
26{
27 cancel();
28 wait();
30
35template <class DBFS>
37{
38 m_mutexQueue.lock();
39 m_clearQueue.clear();
40 m_mutexQueue.unlock();
41
42 QMutexLocker locker(&m_mutexState);
43 m_scanning = false;
45
51template <class DBFS>
54 QMutexLocker locker(&m_mutexState);
55 return m_scanning;
56}
57
58
63template <class DBFS>
65{
66 QMutexLocker locker(&m_mutexQueue);
67 return !m_clearQueue.isEmpty();
68}
69
70
78template <class DBFS>
80{
81 QMutexLocker locker(&m_mutexState);
82 m_scanning = scan;
83
84 // Restart thread if not already running
85 if (!isRunning())
86 start();
87}
88
89
97template <class DBFS>
98void ImageScanThread<DBFS>::EnqueueClear(int devId, const QString &action)
99{
100 m_mutexQueue.lock();
101 m_clearQueue << qMakePair(devId, action);
102 m_mutexQueue.unlock();
103
104 ChangeState(false);
105}
106
107
112template <class DBFS>
114{
115 QMutexLocker locker(&m_mutexProgress);
116 return QStringList() << QString::number(static_cast<int>(gCoreContext->IsBackend()))
117 << QString::number(m_progressCount)
118 << QString::number(m_progressTotalCount);
119}
120
127template <class DBFS>
129{
130 RunProlog();
131
132 setPriority(QThread::LowPriority);
133
134 bool at_least_once { true };
135 while (ClearsPending() || at_least_once)
136 {
137 at_least_once = false;
138
139 // Process all clears before scanning
140 while (ClearsPending())
141 {
142 m_mutexQueue.lock();
143 if (m_clearQueue.isEmpty())
144 break;
145 ClearTask task = m_clearQueue.takeFirst();
146 m_mutexQueue.unlock();
147
148 int devId = task.first;
149 QString action = task.second;
150
151 LOG(VB_GENERAL, LOG_INFO,
152 QString("Clearing Filesystem: %1 %2").arg(action).arg(devId));
153
154 // Clear Db
155 m_dbfs.ClearDb(devId, action);
156
157 // Pass on to thumb generator now scanning has stopped
158 m_thumb.ClearThumbs(devId, action);
159 }
160
161 // Scan requested ?
162 if (IsScanning())
163 {
164 LOG(VB_GENERAL, LOG_INFO, "Starting scan");
165
166 // Load known directories and files from the database
167 if (!m_dbfs.ReadAllImages(m_dbFileMap, m_dbDirMap))
168 // Abort on any Db error
169 break;
170
171 bool firstScan = m_dbFileMap.isEmpty();
172
173 // Pause thumb generator so that scans are fast as possible
174 m_thumb.PauseBackground(true);
175
176 // Adapter determines list of dirs to scan
177 StringMap paths = m_dbfs.GetScanDirs();
178
179 CountFiles(paths.values());
180
181 // Now start the actual syncronization
182 m_seenFile.clear();
183 m_changedImages.clear();
184 StringMap::const_iterator i = paths.constBegin();
185 while (i != paths.constEnd() && IsScanning())
186 {
187 LOG(VB_FILE, LOG_INFO, QString("Synchronizing %1").arg(i.value()));
188 QElapsedTimer timer;
189 timer.start();
190 SyncSubTree(QFileInfo(i.value()), GALLERY_DB_ID, i.key(), i.value());
191 ++i;
192 LOG(VB_FILE, LOG_INFO, QString("Synchronize took %2 seconds")
193 .arg(timer.elapsed()/1000));
194 }
195
196 // Release thumb generator asap
197 m_thumb.PauseBackground(false);
198
199 // Adding or updating directories has been completed.
200 // The maps now only contain old directories & files that are not
201 // in the filesystem anymore. Remove them from the database
202 m_dbfs.RemoveFromDB(QVector<ImagePtr>::fromList(m_dbDirMap.values()));
203 m_dbfs.RemoveFromDB(QVector<ImagePtr>::fromList(m_dbFileMap.values()));
204
205 // Cleanup thumbnails
206 QStringList mesg(m_thumb.DeleteThumbs(QVector<ImagePtr>::fromList(m_dbFileMap.values())));
207 mesg << m_changedImages.join(",");
208
209 // Cleanup dirs
210 m_dbFileMap.clear();
211 m_dbDirMap.clear();
212 m_seenDir.clear();
213
214 m_mutexProgress.lock();
215 // (count == total) signals scan end
216 Broadcast(m_progressTotalCount);
217 // Must reset counts for scan queries
218 m_progressCount = m_progressTotalCount = 0;
219 m_mutexProgress.unlock();
220
221 LOG(VB_GENERAL, LOG_INFO, "Finished scan");
222
223 // For initial scans pause briefly to give thumb generator a headstart
224 // before being deluged by client requests
225 if (firstScan)
226 std::this_thread::sleep_for(1s);
227
228 // Notify clients of completion with removed & changed images
229 m_dbfs.Notify("IMAGE_DB_CHANGED", mesg);
230
231 ChangeState(false);
232 }
233 }
234
235 RunEpilog();
236}
237
238
248template <class DBFS>
249void ImageScanThread<DBFS>::SyncSubTree(const QFileInfo &dirInfo, int parentId,
250 int devId, const QString &base)
251{
252 // Ignore excluded dirs
253 if (m_exclusions.match(dirInfo.fileName()).hasMatch())
254 {
255 LOG(VB_FILE, LOG_INFO,
256 QString("Excluding dir %1").arg(dirInfo.absoluteFilePath()));
257 return;
258 }
259
260 // Use global image filters
261 QDir dir = m_dir;
262 if (!dir.cd(dirInfo.absoluteFilePath()))
263 {
264 LOG(VB_FILE, LOG_INFO,
265 QString("Failed to open dir %1").arg(dirInfo.absoluteFilePath()));
266 return;
267 }
268
269 // Create directory node
270 int id = SyncDirectory(dirInfo, devId, base, parentId);
271 if (id == -1)
272 {
273 LOG(VB_FILE, LOG_INFO,
274 QString("Failed to sync dir %1").arg(dirInfo.absoluteFilePath()));
275 return;
276 }
277
278 // Sync its contents
279 QFileInfoList entries = dir.entryInfoList();
280 for (const auto & fileInfo : std::as_const(entries))
281 {
282 if (!IsScanning())
283 {
284 LOG(VB_GENERAL, LOG_INFO,
285 QString("Scan interrupted in %2").arg(dirInfo.absoluteFilePath()));
286 return;
287 }
288
289 if (fileInfo.isDir())
290 {
291 // Scan this directory
292 SyncSubTree(fileInfo, id, devId, base);
293 }
294 else
295 {
296 SyncFile(fileInfo, devId, base, id);
297
298 QMutexLocker locker(&m_mutexProgress);
299 ++m_progressCount;
300
301 // Throttle updates
302 if (m_bcastTimer.elapsed() > 250)
303 Broadcast(m_progressCount);
304 }
305 }
306}
307
308
323template <class DBFS>
324 int ImageScanThread<DBFS>::SyncDirectory(const QFileInfo &dirInfo, int devId, const QString &base, int parentId)
325{
326 QString absFilePath = dirInfo.absoluteFilePath();
327
328 LOG(VB_FILE, LOG_DEBUG, QString("Syncing directory %1").arg(absFilePath));
329
330 ImagePtr dir(m_dbfs.CreateItem(dirInfo, parentId, devId, base));
331
332 // Is dir already in Db ?
333 if (m_dbDirMap.contains(dir->m_filePath))
334 {
335 ImagePtr dbDir = m_dbDirMap.value(dir->m_filePath);
336 if (dbDir == nullptr)
337 return -1;
338
339 // The directory already exists in the db. Retain its id
340 dir->m_id = dbDir->m_id;
341
342 // Parent may have changed due to a move
343 if (dir->m_modTime != dbDir->m_modTime
344 || dir->m_parentId != dbDir->m_parentId)
345 {
346 LOG(VB_FILE, LOG_INFO,
347 QString("Changed directory %1").arg(absFilePath));
348
349 // Retain existing id & settings
350 dir->m_isHidden = dbDir->m_isHidden;
351 dir->m_userThumbnail = dbDir->m_userThumbnail;
352
353 m_dbfs.UpdateDbImage(*dir);
354 // Note modified images
355 m_changedImages << QString::number(dir->m_id);
356 }
357
358 // Remove the entry from the dbList
359 m_dbDirMap.remove(dir->m_filePath);
360 }
361 // Detect clones (same path in different SG dir)
362 else if (m_seenDir.contains(dir->m_filePath))
363 {
364 ImagePtr cloneDir = m_seenDir.value(dir->m_filePath);
365 if (cloneDir == nullptr)
366 return -1;
367
368 // All clones point to same Db dir. Use latest
369 if (cloneDir->m_modTime >= dir->m_modTime )
370 {
371 LOG(VB_FILE, LOG_INFO, QString("Directory %1 is an older clone of %2")
372 .arg(absFilePath, cloneDir->m_filePath));
373
374 // Use previous version
375 dir = cloneDir;
376 }
377 else
378 {
379 LOG(VB_FILE, LOG_INFO,
380 QString("Directory %1 is a more recent clone of %2")
381 .arg(absFilePath, cloneDir->m_filePath));
382
383 // Use new version
384 dir->m_id = cloneDir->m_id;
385 // Note modified time
386 m_changedImages << QString::number(dir->m_id);
387 }
388
389 // Mark non-devices as cloned (for info display only)
390 if (!dir->IsDevice())
391 {
392 dir->m_type = kCloneDir;
393 m_dbfs.UpdateDbImage(*dir);
394 }
395 }
396 else
397 {
398 LOG(VB_FILE, LOG_INFO, QString("New directory %1").arg(absFilePath));
399
400 // Create new Db dir with new id
401 dir->m_id = m_dbfs.InsertDbImage(*dir);
402 }
403
404 // Note it for clone detection
405 m_seenDir.insert(dir->m_filePath, dir);
406
407 return dir->m_id;
408}
409
410
419template <class DBFS>
421 const QString &path, int type, QString &comment,
422 std::chrono::seconds &time,
423 int &orientation)
424{
425 // Set orientation, date, comment from file meta data
426 ImageMetaData *metadata = (type == kImageFile)
429
430 orientation = metadata->GetOrientation();
431 comment = metadata->GetComment().simplified();
432 QDateTime dt = metadata->GetOriginalDateTime();
433 time = (dt.isValid()) ? std::chrono::seconds(dt.toSecsSinceEpoch()) : 0s;
434
435 delete metadata;
436}
437
438
453template <class DBFS>
454void ImageScanThread<DBFS>::SyncFile(const QFileInfo &fileInfo, int devId,
455 const QString &base, int parentId)
456{
457 // Ignore excluded files
458 if (m_exclusions.match(fileInfo.fileName()).hasMatch())
459 {
460 LOG(VB_FILE, LOG_INFO,
461 QString("Excluding file %1").arg(fileInfo.absoluteFilePath()));
462 return;
463 }
464
465 QString absFilePath = fileInfo.absoluteFilePath();
466
467 ImagePtr im(m_dbfs.CreateItem(fileInfo, parentId, devId, base));
468 if (!im)
469 // Ignore unknown file type
470 return;
471
472 if (m_dbFileMap.contains(im->m_filePath))
473 {
474 ImagePtrK dbIm = m_dbFileMap.value(im->m_filePath);
475
476 // Parent may have changed due to a move
477 if (im->m_modTime == dbIm->m_modTime && im->m_parentId == dbIm->m_parentId)
478 {
479 // File already known & hasn't changed
480 // Remove it from removed list
481 m_dbFileMap.remove(im->m_filePath);
482 // Detect duplicates
483 m_seenFile.insert(im->m_filePath, absFilePath);
484 return;
485 }
486
487 LOG(VB_FILE, LOG_INFO, QString("Modified file %1").arg(absFilePath));
488
489 // Retain existing id & settings
490 im->m_id = dbIm->m_id;
491 im->m_isHidden = dbIm->m_isHidden;
492
493 // Set date, comment from file meta data
494 int fileOrient = 0;
495 PopulateMetadata(absFilePath, im->m_type,
496 im->m_comment, im->m_date, fileOrient);
497
498 // Reset file orientation, retaining existing setting
499 int currentOrient = Orientation(dbIm->m_orientation).GetCurrent();
500 im->m_orientation = Orientation(currentOrient, fileOrient).Composite();
501
502 // Remove it from removed list
503 m_dbFileMap.remove(im->m_filePath);
504 // Note modified images
505 m_changedImages << QString::number(im->m_id);
506
507 // Update db
508 m_dbfs.UpdateDbImage(*im);
509 }
510 else if (m_seenFile.contains(im->m_filePath))
511 {
512 LOG(VB_GENERAL, LOG_WARNING, QString("Ignoring %1 (Duplicate of %2)")
513 .arg(absFilePath, m_seenFile.value(im->m_filePath)));
514 return;
515 }
516 else
517 {
518 // New images will be assigned an id by the db AUTO-INCREMENT
519 LOG(VB_FILE, LOG_INFO, QString("New file %1").arg(absFilePath));
520
521 // Set date, comment from file meta data
522 int fileOrient = 0;
523 PopulateMetadata(absFilePath, im->m_type,
524 im->m_comment, im->m_date, fileOrient);
525
526 // Set file orientation
527 im->m_orientation = Orientation(fileOrient, fileOrient).Composite();
528
529 // Update db (Set id for thumb generator)
530 im->m_id = m_dbfs.InsertDbImage(*im);
531 }
532
533 // Detect duplicate filepaths in SG
534 m_seenFile.insert(im->m_filePath, absFilePath);
535
536 // Populate absolute filename so that thumbgen doesn't need to locate file
537 im->m_filePath = absFilePath;
538
539 // Ensure thumbnail exists.
540 m_thumb.CreateThumbnail(im);
541}
542
543
549template <class DBFS>
551{
552 QFileInfoList entries = dir.entryInfoList();
553 for (const auto & fileInfo : std::as_const(entries))
554 {
555 // Ignore excluded dirs/files
556 if (m_exclusions.match(fileInfo.fileName()).hasMatch())
557 continue;
558
559 if (fileInfo.isFile())
560 {
561 ++m_progressTotalCount;
562 }
563 // Ignore missing dirs
564 else if (dir.cd(fileInfo.fileName()))
565 {
566 CountTree(dir);
567 dir.cdUp();
568 }
569 }
570}
571
572
577template <class DBFS>
579{
580 // Get exclusions as comma-seperated list using glob chars * and ?
581 QString excPattern = gCoreContext->GetSetting("GalleryIgnoreFilter", "");
582
583 // Combine into a single regexp
584 excPattern.replace(".", "\\."); // Preserve "."
585 excPattern.replace("*", ".*"); // Convert glob wildcard "*"
586 excPattern.replace("?", "."); // Convert glob wildcard "?"
587 excPattern.replace(",", "|"); // Convert list to OR's
588
589 QString pattern = QString("^(%1)$").arg(excPattern);
590 m_exclusions = QRegularExpression(pattern);
591
592 LOG(VB_FILE, LOG_DEBUG, QString("Exclude regexp is \"%1\"").arg(pattern));
593
594 // Lock counts until counting complete
595 QMutexLocker locker(&m_mutexProgress);
596 m_progressCount = 0;
597 m_progressTotalCount = 0;
598
599 // Use global image filters
600 QDir dir = m_dir;
601 for (const auto& sgDir : std::as_const(paths))
602 {
603 // Ignore missing dirs
604 if (!dir.cd(sgDir))
605 continue;
606 LOG(VB_FILE, LOG_INFO, QString("Counting %1").arg(dir.absolutePath()));
607 int startCount {m_progressTotalCount};
608 QElapsedTimer timer;
609 timer.start();
610 CountTree(dir);
611 LOG(VB_FILE, LOG_INFO, QString("Counted %1 files in %2 seconds")
612 .arg(m_progressTotalCount - startCount)
613 .arg(timer.elapsed()/1000));
614 }
615 // 0 signifies a scan start
616 Broadcast(0);
617}
618
619
626template <class DBFS>
628{
629 // Only 2 scanners are ever visible (FE & BE) so use bool as scanner id
630 QStringList status;
631 status << QString::number(static_cast<int>(gCoreContext->IsBackend()))
632 << QString::number(progress)
633 << QString::number(m_progressTotalCount);
634
635 m_dbfs.Notify("IMAGE_SCAN_STATUS", status);
636
637 // Reset broadcast throttle
638 m_bcastTimer.start();
639}
640
641
642// Must define the valid template implementations to generate code for the
643// instantiations (as they are defined in the cpp rather than header).
644// Otherwise the linker will fail with undefined references...
645#include "imagemanager.h"
646template class ImageScanThread<ImageDbLocal>;
647template class ImageScanThread<ImageDbSg>;
Abstract class for image metadata.
Definition: imagemetadata.h:92
virtual QDateTime GetOriginalDateTime(bool *exists=nullptr)=0
static ImageMetaData * FromPicture(const QString &filePath)
Factory to retrieve metadata from pictures.
virtual QString GetComment(bool *exists=nullptr)=0
static ImageMetaData * FromVideo(const QString &filePath)
Factory to retrieve metadata from videos.
virtual int GetOrientation(bool *exists=nullptr)=0
~ImageScanThread() override
bool ClearsPending()
Get status of 'clear device' queue.
void CountTree(QDir &dir)
Counts images in a dir subtree.
void EnqueueClear(int devId, const QString &action)
Queues a 'Clear Device' request, which will be actioned immediately.
void PopulateMetadata(const QString &path, int type, QString &comment, std::chrono::seconds &time, int &orientation)
Read image date, orientation, comment from metadata.
bool IsScanning()
Return current scanner status.
ImageScanThread(DBFS *dbfs, ImageThumb< DBFS > *thumbGen)
Constructor.
int SyncDirectory(const QFileInfo &dirInfo, int devId, const QString &base, int parentId)
Updates/populates db for a dir.
void ChangeState(bool scan)
Run or interrupt scanner.
void SyncFile(const QFileInfo &fileInfo, int devId, const QString &base, int parentId)
Updates/populates db for an image/video file.
QStringList GetProgress()
Returns number of images scanned & total number to scan.
void CountFiles(const QStringList &paths)
Counts images in a list of subtrees.
void SyncSubTree(const QFileInfo &dirInfo, int parentId, int devId, const QString &base)
Scans a dir subtree.
void cancel()
Clears queued items so that the thread can exit.
QPair< int, QString > ClearTask
Definition: imagescanner.h:57
void run() override
Synchronises database to the storage group.
void Broadcast(int progress)
Notify listeners of scan progress.
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:49
QString GetSetting(const QString &key, const QString &defaultval="")
bool IsBackend(void) const
is this process a backend process
Encapsulates Exif orientation processing.
Definition: imagemetadata.h:63
int Composite() const
Encode original & current orientation to a single Db field.
Definition: imagemetadata.h:71
int GetCurrent() const
Determines orientation required for an image.
Manages a collection of images.
Handles Exif/FFMpeg metadata tags for images.
Synchronises image database to filesystem.
QSharedPointer< ImageItemK > ImagePtrK
Definition: imagetypes.h:165
QMap< int, QString > StringMap
Definition: imagetypes.h:63
static constexpr int GALLERY_DB_ID
Definition: imagetypes.h:27
@ kCloneDir
A device sub dir comprised from multiple SG dirs.
Definition: imagetypes.h:37
@ kImageFile
A picture.
Definition: imagetypes.h:39
QSharedPointer< ImageItem > ImagePtr
Definition: imagetypes.h:159
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
static bool isRunning(const char *program)
Returns true if a program containing the specified string is running on this machine.
def scan(profile, smoonURL, gate)
Definition: scan.py:54