MythTV master
imagethumbs.cpp
Go to the documentation of this file.
1#include "imagethumbs.h"
2
3#include <QDir>
4#include <QStringList>
5
7#include "libmythbase/mythdirs.h" // for GetAppBinDir
10#include "libmythui/mythimage.h"
11
12#include "imagemetadata.h"
13
17template <class DBFS>
19{
20 cancel();
21 wait();
22}
23
24
28template <class DBFS>
30{
31 // Clear all queues
32 QMutexLocker locker(&m_mutex);
33 m_requestQ.clear();
34 m_backgroundQ.clear();
35}
36
37
42template <class DBFS>
44{
45 if (task)
46 {
47 bool background = task->m_priority > kBackgroundPriority;
48
49 QMutexLocker locker(&m_mutex);
50 if (background)
51 m_backgroundQ.insert(task->m_priority, task);
52 else
53 m_requestQ.insert(task->m_priority, task);
54
55 // restart if not already running
56 if ((m_doBackground || !background) && !this->isRunning())
57 this->start();
58 }
59}
60
61
66template <class DBFS>
67void ThumbThread<DBFS>::AbortDevice(int devId, const QString &action)
68{
69 if (action == "DEVICE CLOSE ALL" || action == "DEVICE CLEAR ALL")
70 {
71 if (isRunning())
72 LOG(VB_FILE, LOG_INFO,
73 QString("Aborting all thumbnails %1").arg(action));
74
75 // Abort thumbnail generation for all devices
76 cancel();
77 return;
78 }
79
80 LOG(VB_FILE, LOG_INFO, QString("Aborting thumbnails (%1) for '%2'")
81 .arg(action).arg(m_dbfs.DeviceName(devId)));
82
83 // Remove all tasks for specific device from every queue
84 QMutexLocker locker(&m_mutex);
85 RemoveTasks(m_requestQ, devId);
86 RemoveTasks(m_backgroundQ, devId);
87 if (isRunning())
88 // Wait until current task is complete - it may be using the device
89 m_taskDone.wait(&m_mutex, 3000);
90}
91
92
93#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
94#define QMutableMultiMapIterator QMutableMapIterator
95#endif
96
100template <class DBFS>
103 QMutableMultiMapIterator<int, TaskPtr> it(queue);
104 while (it.hasNext())
106 it.next();
107 TaskPtr task = it.value();
108 // All thumbs in a task come from same device
109 if (task && !task->m_images.isEmpty()
110 && task->m_images.at(0)->m_device == devId)
111 it.remove();
112 }
115
122template <class DBFS>
124{
125 RunProlog();
126
127 setPriority(QThread::LowestPriority);
128
129 while (true)
131 // Signal previous task is complete (its files have been closed)
132 m_taskDone.wakeAll();
134 // Do all we can to run in background
135 QThread::yieldCurrentThread();
136
137 // process next highest-priority task
140 QMutexLocker locker(&m_mutex);
141 if (!m_requestQ.isEmpty())
142 task = m_requestQ.take(m_requestQ.constBegin().key());
143 else if (m_doBackground && !m_backgroundQ.isEmpty())
144 task = m_backgroundQ.take(m_backgroundQ.constBegin().key());
145 else
146 // quit when both queues exhausted
147 break;
148 }
149
150 // Shouldn't receive empty requests
151 if (task->m_images.isEmpty())
152 continue;
153
154 if (task->m_action == "CREATE")
155 {
156 ImagePtrK im = task->m_images.at(0);
157
158 QString err = CreateThumbnail(im, task->m_priority);
159
160 if (!err.isEmpty())
161 {
162 LOG(VB_GENERAL, LOG_ERR, QString("%1").arg(err));
163 }
164 else if (task->m_notify)
165 {
166 // notify clients when done
167 m_dbfs.Notify("THUMB_AVAILABLE",
168 QStringList(QString::number(im->m_id)));
169 }
170 }
171 else if (task->m_action == "DELETE")
172 {
173 for (const auto& im : std::as_const(task->m_images))
174 {
175 QString thumbnail = im->m_thumbPath;
176 if (!QDir::root().remove(thumbnail))
177 {
178 LOG(VB_FILE, LOG_WARNING,
179 QString("Failed to delete thumbnail %1").arg(thumbnail));
180 continue;
181 }
182 LOG(VB_FILE, LOG_DEBUG,
183 QString("Deleted thumbnail %1").arg(thumbnail));
184
185 // Clean up empty dirs
186 QString path = QFileInfo(thumbnail).path();
187 if (QDir::root().rmpath(path))
188 LOG(VB_FILE, LOG_DEBUG,
189 QString("Cleaned up path %1").arg(path));
190 }
191 }
192 else if (task->m_action == "MOVE")
193 {
194 for (const auto& im : std::as_const(task->m_images))
195 {
196 // Build new thumb path
197 QString newThumbPath =
198 m_dbfs.GetAbsThumbPath(m_dbfs.ThumbDir(im->m_device),
199 m_dbfs.ThumbPath(*im.data()));
200
201 // Ensure path exists
202 if (QDir::root().mkpath(QFileInfo(newThumbPath).path())
203 && QFile::rename(im->m_thumbPath, newThumbPath))
204 {
205 LOG(VB_FILE, LOG_DEBUG, QString("Moved thumbnail %1 -> %2")
206 .arg(im->m_thumbPath, newThumbPath));
207 }
208 else
209 {
210 LOG(VB_FILE, LOG_WARNING,
211 QString("Failed to rename thumbnail %1 -> %2")
212 .arg(im->m_thumbPath, newThumbPath));
213 continue;
214 }
215
216 // Clean up empty dirs
217 QString path = QFileInfo(im->m_thumbPath).path();
218 if (QDir::root().rmpath(path))
219 LOG(VB_FILE, LOG_DEBUG,
220 QString("Cleaned up path %1").arg(path));
221 }
222 }
223 else
224 {
225 LOG(VB_GENERAL, LOG_ERR,
226 QString("Unknown task %1").arg(task->m_action));
227 }
228 }
229
230 RunEpilog();
231}
232
233
239template <class DBFS>
240QString ThumbThread<DBFS>::CreateThumbnail(const ImagePtrK &im, int thumbPriority)
241{
242 if (QDir::root().exists(im->m_thumbPath))
243 {
244 LOG(VB_FILE, LOG_DEBUG, QString("[%3] %2 already exists")
245 .arg(im->m_thumbPath).arg(thumbPriority));
246 return {}; // Notify anyway
247 }
248
249 // Local filenames are always absolute
250 // Remote filenames are absolute from the scanner only
251 // UI requests (derived from Db) are relative
252 QString imagePath = m_dbfs.GetAbsFilePath(im);
253 if (imagePath.isEmpty())
254 return QString("Empty image path: %1").arg(im->m_filePath);
255
256 // Ensure path exists
257 QDir::root().mkpath(QFileInfo(im->m_thumbPath).path());
258
259 QImage image;
260 if (im->m_type == kImageFile)
261 {
262 if (!image.load(imagePath))
263 return QString("Failed to open image %1").arg(imagePath);
264
265 // Resize to optimise load/display time by FE's
266 image = image.scaled(QSize(240,180), Qt::KeepAspectRatio, Qt::SmoothTransformation);
267 }
268 else if (im->m_type == kVideoFile)
269 {
270 // Run Preview Generator in foreground
272 QStringList args;
273 args << "--size 320x240"; // Video thumbnails are also shown in slideshow
274 args << QString("--infile '%1'").arg(imagePath);
275 args << QString("--outfile '%1'").arg(im->m_thumbPath);
276
277 MythSystemLegacy ms(cmd, args,
284 ms.SetNice(10);
285 ms.SetIOPrio(7);
286 ms.Run(30s);
287 if (ms.Wait() != GENERIC_EXIT_OK)
288 {
289 LOG(VB_GENERAL, LOG_ERR, QString("Failed to run %2 %3")
290 .arg(cmd, args.join(" ")));
291 return QString("Preview Generator failed for %1").arg(imagePath);
292 }
293
294 if (!image.load(im->m_thumbPath))
295 return QString("Failed to open preview %1").arg(im->m_thumbPath);
296 }
297 else
298 {
299 return QString("Can't create thumbnail for type %1 (image %2)")
300 .arg(im->m_type).arg(imagePath);
301 }
302
303 int orientBy = Orientation(im->m_orientation).GetCurrent();
304
305 // Orientate now to optimise load/display time - no orientation
306 // is required when displaying thumbnails
307 image = MythImage::ApplyExifOrientation(image, orientBy);
308
309 // Create the thumbnail
310 if (!image.save(im->m_thumbPath))
311 return QString("Failed to create thumbnail %1").arg(im->m_thumbPath);
312
313 LOG(VB_FILE, LOG_INFO, QString("[%2] Created %1")
314 .arg(im->m_thumbPath).arg(thumbPriority));
315 return {};
316}
317
318
322template <class DBFS>
324{
325 QMutexLocker locker(&m_mutex);
326 m_doBackground = !pause;
327
328 // restart if not already running
329 if (m_doBackground && !this->isRunning())
330 this->start();
331}
332
333
337template <class DBFS>
339 : m_dbfs(*dbfs),
340 m_imageThread(new ThumbThread<DBFS>("ImageThumbs", dbfs)),
341 m_videoThread(new ThumbThread<DBFS>("VideoThumbs", dbfs))
342{}
343
344
348template <class DBFS>
350{
351 delete m_imageThread;
352 delete m_videoThread;
353 m_imageThread = m_videoThread = nullptr;
354}
355
356
363template <class DBFS>
364void ImageThumb<DBFS>::ClearThumbs(int devId, const QString &action)
365{
366 // Cancel pending requests for the device
367 // Waits for current generator task to complete
368 if (m_imageThread)
369 m_imageThread->AbortDevice(devId, action);
370 if (m_videoThread)
371 m_videoThread->AbortDevice(devId, action);
372
373 // Remove devices now they are not in use
374 QStringList mountPaths = m_dbfs.CloseDevices(devId, action);
375// if (mountPaths.isEmpty())
376// return;
377
378 // Generate file & thumbnail urls (as per image cache) of mountpoints
379 QStringList mesg;
380 for (const auto& mount : std::as_const(mountPaths))
381 mesg << m_dbfs.MakeFileUrl(mount)
382 << m_dbfs.MakeThumbUrl(mount);
383
384 // Notify clients to clear cache
385 m_dbfs.Notify("IMAGE_DEVICE_CHANGED", mesg);
386}
387
388
394template <class DBFS>
396{
397 // Determine affected images and redundant images/thumbnails
398 QStringList ids;
399
400 // Pictures & videos are deleted by their own threads
401 ImageListK pics;
402 ImageListK videos;
403 for (const auto& im : std::as_const(images))
404 {
405 if (im->m_type == kVideoFile)
406 videos.append(im);
407 else
408 pics.append(im);
409
410 ids << QString::number(im->m_id);
411 }
412
413 if (!pics.isEmpty() && m_imageThread)
414 m_imageThread->Enqueue(TaskPtr(new ThumbTask("DELETE", pics)));
415 if (!videos.isEmpty() && m_videoThread)
416 m_videoThread->Enqueue(TaskPtr(new ThumbTask("DELETE", videos)));
417 return ids.join(",");
418}
419
420
427template <class DBFS>
428void ImageThumb<DBFS>::CreateThumbnail(const ImagePtrK &im, int priority,
429 bool notify)
430{
431 if (!im)
432 return;
433
434 // Allocate task a (hopefully) unique priority
435 if (priority == kBackgroundPriority)
436 priority = Priority(*im);
437
438 TaskPtr task(new ThumbTask("CREATE", im, priority, notify));
439
440 if (im->m_type == kImageFile && m_imageThread)
441 {
442 m_imageThread->Enqueue(task);
443 }
444 else if (im->m_type == kVideoFile && m_videoThread)
445 {
446 m_videoThread->Enqueue(task);
447 }
448 else
449 {
450 LOG(VB_FILE, LOG_INFO, QString("Ignoring create thumbnail %1, type %2")
451 .arg(im->m_id).arg(im->m_type));
452 }
453}
454
455
460template <class DBFS>
462{
463 if (!im)
464 return;
465
466 TaskPtr task(new ThumbTask("MOVE", im));
467
468 if (im->m_type == kImageFile && m_imageThread)
469 {
470 m_imageThread->Enqueue(task);
471 }
472 else if (im->m_type == kVideoFile && m_videoThread)
473 {
474 m_videoThread->Enqueue(task);
475 }
476 else
477 {
478 LOG(VB_FILE, LOG_INFO, QString("Ignoring move thumbnail %1, type %2")
479 .arg(im->m_id).arg(im->m_type));
480 }
481}
482
483
487template <class DBFS>
489{
490 LOG(VB_FILE, LOG_INFO, QString("Paused %1").arg(pause));
491
492 if (m_imageThread)
493 m_imageThread->PauseBackground(pause);
494 if (m_videoThread)
495 m_videoThread->PauseBackground(pause);
496}
497
498
499// Must define the valid template implementations to generate code for the
500// instantiations (as they are defined in the cpp rather than header).
501// Otherwise the linker will fail with undefined references...
502#include "imagemanager.h"
503template class ImageThumb<ImageDbLocal>;
504template class ImageThumb<ImageDbSg>;
void CreateThumbnail(const ImagePtrK &im, int priority=kBackgroundPriority, bool notify=false)
Creates a thumbnail.
void MoveThumbnail(const ImagePtrK &im)
Renames a thumbnail.
void PauseBackground(bool pause)
Pauses or restarts processing of background tasks (scanner requests)
QString DeleteThumbs(const ImageList &images)
Remove specific thumbnails.
void ClearThumbs(int devId, const QString &action)
Clears thumbnails for a device.
~ImageThumb()
Destructor .
ImageThumb(DBFS *dbfs)
Constructor.
static QImage ApplyExifOrientation(QImage &image, int orientation)
Definition: mythimage.cpp:97
uint Wait(std::chrono::seconds timeout=0s)
bool SetNice(int nice)
void Run(std::chrono::seconds timeout=0s)
Runs a command inside the /bin/sh shell. Returns immediately.
bool SetIOPrio(int prio)
Encapsulates Exif orientation processing.
Definition: imagemetadata.h:63
int GetCurrent() const
Determines orientation required for an image.
A generator request that is queued.
Definition: imagethumbs.h:42
A generator worker thread.
Definition: imagethumbs.h:88
QString CreateThumbnail(const ImagePtrK &im, int thumbPriority)
Generate thumbnail for an image.
void run() override
Handles thumbnail requests by priority.
~ThumbThread() override
Destructor.
Definition: imagethumbs.cpp:18
QMultiMap< int, TaskPtr > ThumbQueue
A priority queue where 0 is highest priority.
Definition: imagethumbs.h:111
void PauseBackground(bool pause)
Pauses or restarts processing of background tasks (scanner requests)
static void RemoveTasks(ThumbQueue &queue, int devId)
void cancel()
Clears all queues so that the thread will terminate.
Definition: imagethumbs.cpp:29
void AbortDevice(int devId, const QString &action)
Clears thumbnail request queue.
Definition: imagethumbs.cpp:67
void Enqueue(const TaskPtr &task)
Queues a Create request.
Definition: imagethumbs.cpp:43
@ GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:13
Manages a collection of images.
Handles Exif/FFMpeg metadata tags for images.
Creates and manages thumbnails.
QSharedPointer< ThumbTask > TaskPtr
Definition: imagethumbs.h:82
@ kBackgroundPriority
Scanner background request.
Definition: imagethumbs.h:36
QVector< ImagePtr > ImageList
Definition: imagetypes.h:160
QList< ImagePtrK > ImageListK
Definition: imagetypes.h:166
QSharedPointer< ImageItemK > ImagePtrK
Definition: imagetypes.h:165
@ kImageFile
A picture.
Definition: imagetypes.h:39
@ kVideoFile
A video.
Definition: imagetypes.h:40
static constexpr const char * MYTH_APPNAME_MYTHPREVIEWGEN
Definition: mythappname.h:11
QString GetAppBinDir(void)
Definition: mythdirs.cpp:260
#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.
@ kMSDontBlockInputDevs
avoid blocking LIRC & Joystick Menu
Definition: mythsystem.h:36
@ kMSProcessEvents
process events while waiting
Definition: mythsystem.h:39
@ kMSPropagateLogs
add arguments for MythTV log propagation
Definition: mythsystem.h:52
@ kMSRunShell
run process through shell
Definition: mythsystem.h:43
@ kMSDontDisableDrawing
avoid disabling UI drawing
Definition: mythsystem.h:37
@ kMSAutoCleanup
automatically delete if backgrounded
Definition: mythsystem.h:45
bool exists(str path)
Definition: xbmcvfs.py:51