From b28dba6087f0433af8ece176b64fcac54ca370a4 Mon Sep 17 00:00:00 2001 From: Berthold Stoeger Date: Tue, 15 May 2018 20:47:35 +0200 Subject: [PATCH] Dive pictures: Recognize video files When generating thumbnails, test for video files. If it is, use a dummy-thumbnail. Write only the type (video), but no image to the thumbnail cache, for forward-compatibility. Signed-off-by: Berthold Stoeger --- core/imagedownloader.cpp | 105 ++++++++++++++++++++++----------------- core/imagedownloader.h | 11 ++++ core/metadata.h | 5 +- core/qthelper.cpp | 8 +++ core/qthelper.h | 1 + 5 files changed, 83 insertions(+), 47 deletions(-) diff --git a/core/imagedownloader.cpp b/core/imagedownloader.cpp index 1cdef0b52..141cc79d3 100644 --- a/core/imagedownloader.cpp +++ b/core/imagedownloader.cpp @@ -69,27 +69,40 @@ void ImageDownloader::saveImage(QNetworkReply *reply) reply->deleteLater(); } -// Fetch a picture from the given filename. If this is a non-remote filename, fetch it from disk. -// Remote files are fetched from the net in a background thread. In such a case, the output-flag -// "stillLoading" is set to true. +static bool isVideoFile(const QString &filename) +{ + // Currently, we're very crude. Simply check if the file exists and if it + // is an MP4-style format. + QFileInfo fi(filename); + if (!fi.exists() && !fi.isFile()) + return false; + metadata md; + return get_metadata(qPrintable(filename), &md) == MEDIATYPE_VIDEO; +} + +// Fetch a picture from the given filename and determine its type (picture of video). +// If this is a non-remote file, fetch it from disk. Remote files are fetched from the +// net in a background thread. In such a case, the output-type is set to MEDIATYPE_STILL_LOADING. // If the input-flag "tryDownload" is set to false, no download attempt is made. This is to // prevent infinite loops, where failed image downloads would be repeated ad infinitum. -// Returns: fetched image, stillLoading flag -static std::pair fetchImage(const QString &filename, const QString &originalFilename, bool tryDownload) +// Returns: fetched image, type +Thumbnailer::Thumbnail Thumbnailer::fetchImage(const QString &filename, const QString &originalFilename, bool tryDownload) { - QImage thumb; - bool stillLoading = false; QUrl url = QUrl::fromUserInput(filename); if (url.isLocalFile()) { - thumb.load(url.toLocalFile()); + QString filename = url.toLocalFile(); + if (isVideoFile(filename)) + return { videoImage, MEDIATYPE_VIDEO }; + QImage thumb(filename); + return { thumb, thumb.isNull() ? MEDIATYPE_IO_ERROR : MEDIATYPE_PICTURE }; } else if (tryDownload) { // This has to be done in UI main thread, because QNetworkManager refuses // to treat requests from other threads. invokeMethod() is Qt's way of calling a // function in a different thread, namely the thread the called object is associated to. QMetaObject::invokeMethod(ImageDownloader::instance(), "load", Qt::AutoConnection, Q_ARG(QUrl, url), Q_ARG(QString, originalFilename)); - stillLoading = true; + return { QImage(), MEDIATYPE_STILL_LOADING }; } - return { thumb, stillLoading }; + return { QImage(), MEDIATYPE_IO_ERROR }; } // Fetch a picture based on its original filename. If there is a translated filename (obtained either @@ -98,25 +111,24 @@ static std::pair fetchImage(const QString &filename, const QString // was downloaded previously, but for some reason the cached picture was lost. Therefore, in such a // case, try the canonical filename. If that likewise fails, give up. For input and output parameters // see fetchImage() above. -static std::pair getHashedImage(const QString &filename, bool tryDownload) +Thumbnailer::Thumbnail Thumbnailer::getHashedImage(const QString &filename, bool tryDownload) { - QImage thumb; - bool stillLoading = false; QString localFilename = localFilePath(filename); // If there is a translated filename, try that first + Thumbnail thumbnail { QImage(), MEDIATYPE_UNKNOWN }; if (localFilename != filename) - std::tie(thumb, stillLoading) = fetchImage(localFilename, filename, tryDownload); + thumbnail = fetchImage(localFilename, filename, tryDownload); // Note that the translated filename should never be a remote file and therefore checking for - // stillLoading is currently not necessary. But in the future, we might support such a use case + // still-loading is currently not necessary. But in the future, we might support such a use case // (e.g. images stored in the cloud). - if (thumb.isNull() && !stillLoading) - std::tie(thumb, stillLoading) = fetchImage(filename, filename, tryDownload); + if (thumbnail.img.isNull() && thumbnail.type != MEDIATYPE_STILL_LOADING) + thumbnail = fetchImage(filename, filename, tryDownload); - if (thumb.isNull() && !stillLoading) + if (thumbnail.img.isNull() && thumbnail.type != MEDIATYPE_STILL_LOADING) qInfo() << "Error loading image" << filename << "[local:" << localFilename << "]"; - return { thumb, stillLoading }; + return thumbnail; } static QImage renderIcon(const char *id, int size) @@ -130,7 +142,8 @@ static QImage renderIcon(const char *id, int size) } Thumbnailer::Thumbnailer() : failImage(renderIcon(":filter-close", maxThumbnailSize())), // TODO: Don't misuse filter close icon - dummyImage(renderIcon(":camera-icon", maxThumbnailSize())) + dummyImage(renderIcon(":camera-icon", maxThumbnailSize())), + videoImage(renderIcon(":video-icon", maxThumbnailSize())) { // Currently, we only process one image at a time. Stefan Fuchs reported problems when // calculating multiple thumbnails at once and this hopefully helps. @@ -145,11 +158,11 @@ Thumbnailer *Thumbnailer::instance() return &self; } -static QImage getThumbnailFromCache(const QString &picture_filename) +Thumbnailer::Thumbnail Thumbnailer::getThumbnailFromCache(const QString &picture_filename) { QString filename = thumbnailFileName(picture_filename); if (filename.isEmpty()) - return QImage(); + return { QImage(), MEDIATYPE_UNKNOWN }; QFile file(filename); if (prefs.auto_recalculate_thumbnails) { @@ -163,27 +176,32 @@ static QImage getThumbnailFromCache(const QString &picture_filename) if (pictureTime.isValid() && thumbnailTime.isValid() && thumbnailTime < pictureTime) { // Both files exist, have valid timestamps and thumbnail was calculated before picture. // Return an empty thumbnail to signal recalculation of the thumbnail - return QImage(); + return { QImage(), MEDIATYPE_UNKNOWN }; } } } if (!file.open(QIODevice::ReadOnly)) - return QImage(); + return { QImage(), MEDIATYPE_UNKNOWN }; QDataStream stream(&file); // Each thumbnail file is composed of a media-type and an image file. - // Currently, the type is ignored. This will be used to mark videos. quint32 type; QImage res; stream >> type; stream >> res; - return res; + + // Thumbnails of videos currently not supported - replace by dummy + // TODO: Perhaps extract thumbnails + if (type == MEDIATYPE_VIDEO) + res = videoImage; + + return { res, (mediatype_t)type }; } -static void addThumbnailToCache(const QImage &thumbnail, const QString &picture_filename) +void Thumbnailer::addThumbnailToCache(const Thumbnail &thumbnail, const QString &picture_filename) { - if (thumbnail.isNull()) + if (thumbnail.img.isNull()) return; QString filename = thumbnailFileName(picture_filename); @@ -192,51 +210,48 @@ static void addThumbnailToCache(const QImage &thumbnail, const QString &picture_ return; QDataStream stream(&file); - // For format of the file, see comments in getThumnailForCache - quint32 type = MEDIATYPE_PICTURE; - stream << type; - stream << thumbnail; + stream << (quint32)thumbnail.type; + if (thumbnail.type == MEDIATYPE_PICTURE) // TODO: Perhaps also support caching of video thumbnails + stream << thumbnail.img; file.commit(); } void Thumbnailer::recalculate(QString filename) { - auto res = getHashedImage(filename, true); + Thumbnail thumbnail = getHashedImage(filename, true); // If we couldn't load the image from disk -> leave old thumbnail. // The case "load from web" is a bit inconsistent: it will call into processItem() later // and therefore a "broken" image symbol may be shown. - if (res.second || res.first.isNull()) + if (thumbnail.type == MEDIATYPE_STILL_LOADING || thumbnail.img.isNull()) return; - QImage thumbnail = res.first; addThumbnailToCache(thumbnail, filename); QMutexLocker l(&lock); - emit thumbnailChanged(filename, thumbnail); + emit thumbnailChanged(filename, thumbnail.img); workingOn.remove(filename); } void Thumbnailer::processItem(QString filename, bool tryDownload) { - QImage thumbnail = getThumbnailFromCache(filename); + Thumbnail thumbnail = getThumbnailFromCache(filename); - if (thumbnail.isNull()) { - auto res = getHashedImage(filename, tryDownload); - if (res.second) + if (thumbnail.img.isNull()) { + thumbnail = getHashedImage(filename, tryDownload); + if (thumbnail.type == MEDIATYPE_STILL_LOADING) return; - thumbnail = res.first; - if (thumbnail.isNull()) { - thumbnail = failImage; + if (thumbnail.img.isNull()) { + thumbnail.img = failImage; } else { int size = maxThumbnailSize(); - thumbnail = thumbnail.scaled(size, size, Qt::KeepAspectRatio); + thumbnail.img = thumbnail.img.scaled(size, size, Qt::KeepAspectRatio); addThumbnailToCache(thumbnail, filename); } } QMutexLocker l(&lock); - emit thumbnailChanged(filename, thumbnail); + emit thumbnailChanged(filename, thumbnail.img); workingOn.remove(filename); } diff --git a/core/imagedownloader.h b/core/imagedownloader.h index 13ba80746..d5cb889fe 100644 --- a/core/imagedownloader.h +++ b/core/imagedownloader.h @@ -2,6 +2,7 @@ #ifndef IMAGEDOWNLOADER_H #define IMAGEDOWNLOADER_H +#include "metadata.h" #include #include #include @@ -48,14 +49,24 @@ public slots: signals: void thumbnailChanged(QString filename, QImage thumbnail); private: + struct Thumbnail { + QImage img; + mediatype_t type; + }; + Thumbnailer(); + static void addThumbnailToCache(const Thumbnail &thumbnail, const QString &picture_filename); void recalculate(QString filename); void processItem(QString filename, bool tryDownload); + Thumbnail getThumbnailFromCache(const QString &picture_filename); + Thumbnail fetchImage(const QString &filename, const QString &originalFilename, bool tryDownload); + Thumbnail getHashedImage(const QString &filename, bool tryDownload); mutable QMutex lock; QThreadPool pool; QImage failImage; // Shown when image-fetching fails QImage dummyImage; // Shown before thumbnail is fetched + QImage videoImage; // Place holder for videos QMap> workingOn; }; diff --git a/core/metadata.h b/core/metadata.h index 5cbb865f4..da2a9c3d2 100644 --- a/core/metadata.h +++ b/core/metadata.h @@ -10,10 +10,11 @@ struct metadata { }; enum mediatype_t { - MEDIATYPE_IO_ERROR, // Couldn't read file - MEDIATYPE_UNKNOWN, // Couldn't identify file + MEDIATYPE_UNKNOWN, // Couldn't (yet) identify file + MEDIATYPE_IO_ERROR, // Couldn't read file MEDIATYPE_PICTURE, MEDIATYPE_VIDEO, + MEDIATYPE_STILL_LOADING, // Still processing in the background }; #ifdef __cplusplus diff --git a/core/qthelper.cpp b/core/qthelper.cpp index 299f8771b..ed3de1a11 100644 --- a/core/qthelper.cpp +++ b/core/qthelper.cpp @@ -1209,11 +1209,19 @@ QString localFilePath(const QString &originalFilename) return localFilenameOf.value(originalFilename, originalFilename); } +// TODO: Apparently Qt has no simple way of listing the supported video +// codecs? Do we have to query them by hand using QMediaPlayer::hasSupport()? +const QStringList videoExtensionsList = { + ".avi", ".mp4", ".mpeg", ".mpg", ".wmv" +}; + QStringList imageExtensionFilters() { QStringList filters; foreach (QString format, QImageReader::supportedImageFormats()) { filters.append(QString("*.").append(format)); } + foreach (const QString &format, videoExtensionsList) + filters.append("*" + format); return filters; } diff --git a/core/qthelper.h b/core/qthelper.h index 64a57798d..a2decd527 100644 --- a/core/qthelper.h +++ b/core/qthelper.h @@ -42,6 +42,7 @@ QByteArray getCurrentAppState(); void setCurrentAppState(const QByteArray &state); void init_proxy(); QString getUUID(); +extern const QStringList videoExtensionsList; QStringList imageExtensionFilters(); char *intdup(int index); char *copy_qstring(const QString &);