Jean-Baptiste Kempf pushed to branch master at VideoLAN / VLC


Commits:
19cc1b35 by Prince Gupta at 2022-06-11T15:44:58+00:00
qt: simplify image assignment in roundimage

- - - - -
4082222d by Prince Gupta at 2022-06-11T15:44:58+00:00
qt: support image provider in RoundImage

- - - - -
6191c7d2 by Prince Gupta at 2022-06-11T15:44:58+00:00
qt: implement classes for custom ml cover generation via image provider

- - - - -
8cfcf077 by Prince Gupta at 2022-06-11T15:44:58+00:00
qt: provide custom cover generation via medialib

- - - - -
7bebe5a8 by Prince Gupta at 2022-06-11T15:44:58+00:00
qt: use in-memory caching for custom ml covers

- - - - -
375dd232 by Prince Gupta at 2022-06-11T15:44:58+00:00
qt: remove unused code from CoverGenerator

- - - - -
87e7ee3c by Prince Gupta at 2022-06-11T15:44:58+00:00
qt: reduce custom cover default size

saves memory

- - - - -
c4c2adf8 by Prince Gupta at 2022-06-11T15:44:58+00:00
qt: don't use medialib cover for genre

covers are custom generated

- - - - -
95f4aa56 by Prince Gupta at 2022-06-11T15:44:58+00:00
qt: remove mlitemcover

- - - - -
2aa6e722 by Prince Gupta at 2022-06-11T15:44:58+00:00
qt: improve network image reading in roundimage

removes blocking call when reading network image

- - - - -
04841706 by Prince Gupta at 2022-06-11T15:44:58+00:00
qt: increase cache size of round image

this is done to improve caching of generated cover

- - - - -
43e42fec by Prince Gupta at 2022-06-11T15:44:58+00:00
qt: remove smooth property from cover generator

- - - - -
716ff4b7 by Prince Gupta at 2022-06-11T15:44:58+00:00
        qt: fix image scaling in cover generator

- - - - -


27 changed files:

- modules/gui/qt/Makefile.am
- modules/gui/qt/maininterface/mainui.cpp
- modules/gui/qt/medialibrary/medialib.cpp
- modules/gui/qt/medialibrary/medialib.hpp
- modules/gui/qt/medialibrary/mlbasemodel.hpp
- + modules/gui/qt/medialibrary/mlcustomcover.cpp
- modules/gui/qt/medialibrary/mlitemcover.hpp → 
modules/gui/qt/medialibrary/mlcustomcover.hpp
- modules/gui/qt/medialibrary/mlfolder.cpp
- modules/gui/qt/medialibrary/mlfolder.hpp
- modules/gui/qt/medialibrary/mlgenre.cpp
- modules/gui/qt/medialibrary/mlgenre.hpp
- modules/gui/qt/medialibrary/mlgenremodel.cpp
- modules/gui/qt/medialibrary/mlgroup.cpp
- modules/gui/qt/medialibrary/mlgroup.hpp
- modules/gui/qt/medialibrary/mlhelper.cpp
- modules/gui/qt/medialibrary/mlhelper.hpp
- − modules/gui/qt/medialibrary/mlitemcover.cpp
- modules/gui/qt/medialibrary/mlplaylist.cpp
- modules/gui/qt/medialibrary/mlplaylist.hpp
- modules/gui/qt/medialibrary/mlplaylistlistmodel.cpp
- modules/gui/qt/medialibrary/mlvideofoldersmodel.cpp
- modules/gui/qt/medialibrary/mlvideogroupsmodel.cpp
- modules/gui/qt/util/covergenerator.cpp
- modules/gui/qt/util/covergenerator.hpp
- modules/gui/qt/widgets/native/roundimage.cpp
- modules/gui/qt/widgets/native/roundimage.hpp
- po/POTFILES.in


Changes:

=====================================
modules/gui/qt/Makefile.am
=====================================
@@ -179,8 +179,6 @@ libqt_plugin_la_SOURCES = \
        gui/qt/medialibrary/mlgroup.hpp \
        gui/qt/medialibrary/mlhelper.cpp \
        gui/qt/medialibrary/mlhelper.hpp \
-       gui/qt/medialibrary/mlitemcover.cpp \
-       gui/qt/medialibrary/mlitemcover.hpp \
        gui/qt/medialibrary/mllistcache.cpp \
        gui/qt/medialibrary/mllistcache.hpp \
        gui/qt/medialibrary/mlthreadpool.cpp \
@@ -212,6 +210,8 @@ libqt_plugin_la_SOURCES = \
        gui/qt/medialibrary/mlplaylistmodel.hpp \
        gui/qt/medialibrary/thumbnailcollector.hpp \
        gui/qt/medialibrary/thumbnailcollector.cpp \
+       gui/qt/medialibrary/mlcustomcover.hpp \
+       gui/qt/medialibrary/mlcustomcover.cpp \
        gui/qt/menus/custom_menus.cpp \
        gui/qt/menus/custom_menus.hpp \
        gui/qt/menus/qml_menu_wrapper.cpp \


=====================================
modules/gui/qt/maininterface/mainui.cpp
=====================================
@@ -4,6 +4,7 @@
 
 #include "medialibrary/medialib.hpp"
 #include "medialibrary/mlqmltypes.hpp"
+#include "medialibrary/mlcustomcover.hpp"
 #include "medialibrary/mlalbummodel.hpp"
 #include "medialibrary/mlartistmodel.hpp"
 #include "medialibrary/mlalbumtrackmodel.hpp"
@@ -63,6 +64,8 @@ using  namespace vlc::playlist;
 
 namespace {
 
+const QString MLCUSTOMCOVER_PROVIDERID = "mlcustomcover";
+
 template<class T>
 class SingletonRegisterHelper
 {
@@ -148,6 +151,14 @@ bool MainUI::setup(QQmlEngine* engine)
     engine->setOutputWarningsToStandardError(false);
     connect(engine, &QQmlEngine::warnings, this, &MainUI::onQmlWarning);
 
+    if (m_mainCtx->hasMediaLibrary())
+    {
+        auto customCover = new MLCustomCover(MLCUSTOMCOVER_PROVIDERID, 
m_mainCtx->getMediaLibrary());
+        m_mainCtx->getMediaLibrary()->setCustomCover(customCover);
+
+        engine->addImageProvider(MLCUSTOMCOVER_PROVIDERID, customCover);
+    }
+
     m_component  = new QQmlComponent(engine, 
QStringLiteral("qrc:/main/MainInterface.qml"), 
QQmlComponent::PreferSynchronous, engine);
     if (m_component->isLoading())
     {


=====================================
modules/gui/qt/medialibrary/medialib.cpp
=====================================
@@ -520,3 +520,13 @@ void MediaLib::runOnMLThreadTargetDestroyed(QObject * 
object)
         //no need to disconnect QObject::destroyed, as object is currently 
being destroyed
     }
 }
+
+MLCustomCover *MediaLib::customCover() const
+{
+    return m_customCover;
+}
+
+void MediaLib::setCustomCover(MLCustomCover *newCustomCover)
+{
+    m_customCover = newCustomCover;
+}


=====================================
modules/gui/qt/medialibrary/medialib.hpp
=====================================
@@ -29,6 +29,8 @@
 
 #include "util/qmlinputitem.hpp"
 
+class MLCustomCover;
+
 namespace vlc {
 namespace playlist {
 class Media;
@@ -176,6 +178,9 @@ public:
      */
     void cancelMLTask(const QObject* object, quint64 taskId);
 
+   MLCustomCover *customCover() const;
+   void setCustomCover(MLCustomCover *newCustomCover);
+
 signals:
     void discoveryStarted();
     void discoveryCompleted();
@@ -195,6 +200,7 @@ private slots:
 
 private:
     qt_intf_t* m_intf;
+    MLCustomCover *m_customCover {};
 
     bool m_idle = false;
     bool m_discoveryPending = false;


=====================================
modules/gui/qt/medialibrary/mlbasemodel.hpp
=====================================
@@ -38,8 +38,6 @@
 // Fordward declarations
 class MLListCache;
 class MediaLib;
-class MLItemCover;
-class CoverGenerator;
 
 class MLBaseModel : public QAbstractListModel
 {
@@ -194,11 +192,6 @@ protected:
 
     //loader used to load single items
     std::shared_ptr<BaseLoader> m_itemLoader;
-
-private: // Friends
-    friend QString createGroupMediaCover(const MLBaseModel* model, 
MLItemCover* parent
-                                         , int role
-                                         , const 
std::shared_ptr<CoverGenerator> generator);
 };
 
 #endif // MLBASEMODEL_HPP


=====================================
modules/gui/qt/medialibrary/mlcustomcover.cpp
=====================================
@@ -0,0 +1,307 @@
+/*****************************************************************************
+ * Copyright (C) 2022 VLC authors and VideoLAN
+ *
+ * Authors: Prince Gupta <guptaprince8...@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * ( at your option ) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, 
USA.
+ *****************************************************************************/
+
+#include "mlcustomcover.hpp"
+
+#include "medialibrary/medialib.hpp"
+#include "medialibrary/mlhelper.hpp"
+#include "medialibrary/thumbnailcollector.hpp"
+#include "util/asynctask.hpp"
+#include "util/covergenerator.hpp"
+
+#include <qhashfunctions.h>
+
+#include <QCache>
+#include <QMutex>
+#include <QUrl>
+#include <QUrlQuery>
+
+namespace
+{
+
+const QString ID_KEY = QStringLiteral("id");
+const QString TYPE_KEY = QStringLiteral("type");
+const QString WIDTH_KEY = QStringLiteral("width");
+const QString HEIGHT_KEY = QStringLiteral("height");
+const QString COUNTX_KEY = QStringLiteral("countX");
+const QString COUNTY_KEY = QStringLiteral("countY");
+const QString BLUR_KEY = QStringLiteral("blur");
+const QString SPLIT_KEY = QStringLiteral("split");
+const QString DEFAULT_COVER_KEY = QStringLiteral("default_cover");
+
+struct CoverData
+{
+    MLItemId id;
+    QSize size;
+    int countX;
+    int countY;
+    int blur;
+    int split;
+    QString defaultCover;
+};
+
+QUrlQuery toQuery(const CoverData &data)
+{
+    QUrlQuery query;
+    query.addQueryItem(ID_KEY, QString::number(data.id.id));
+    query.addQueryItem(TYPE_KEY, QString::number(data.id.type));
+    query.addQueryItem(WIDTH_KEY, QString::number(data.size.width()));
+    query.addQueryItem(HEIGHT_KEY, QString::number(data.size.height()));
+    query.addQueryItem(COUNTX_KEY, QString::number(data.countX));
+    query.addQueryItem(COUNTY_KEY, QString::number(data.countY));
+    query.addQueryItem(BLUR_KEY, QString::number(data.blur));
+    query.addQueryItem(SPLIT_KEY, QString::number(data.split));
+    query.addQueryItem(DEFAULT_COVER_KEY, data.defaultCover);
+    return query;
+}
+
+CoverData fromQuery(const QUrlQuery &query, QString *error)
+{
+    try
+    {
+        const auto getValue = [&](const QString &key)
+        {
+            if (!query.hasQueryItem(key))
+                throw QString("key '%1' doesn't exist").arg(key);
+
+            return query.queryItemValue(key);
+        };
+
+        const auto intValue = [&](const QString &key)
+        {
+            auto value = getValue(key);
+            bool ok;
+            int iValue = value.toInt(&ok);
+            if (!ok)
+                throw QString("invalid value for key '%1'").arg(key);
+
+            return iValue;
+        };
+
+        CoverData data;
+        data.id.id = intValue(ID_KEY);
+        data.id.type = static_cast<vlc_ml_parent_type>(intValue(TYPE_KEY));
+        data.size.setWidth(intValue(WIDTH_KEY));
+        data.size.setHeight(intValue(HEIGHT_KEY));
+        data.countX = intValue(COUNTX_KEY);
+        data.countY = intValue(COUNTY_KEY);
+        data.blur = intValue(BLUR_KEY);
+        data.split = intValue(SPLIT_KEY);
+        data.defaultCover = getValue(DEFAULT_COVER_KEY);
+
+        return data;
+    }
+    catch (const QString &e)
+    {
+        if (error)
+            *error = e;
+        return {};
+    }
+}
+
+
+struct ThumbnailList
+{
+    QSet<int64_t> toGenerate;
+    QStringList existing;
+};
+
+QStringList getGenreMediaThumbnails(vlc_medialibrary_t* p_ml, const int count, 
const int64_t id)
+{
+    QStringList thumbnails;
+
+    vlc_ml_query_params_t params {};
+
+    // NOTE: We retrieve twice the count to maximize our chances to get a 
valid thumbnail.
+    params.i_nbResults = count * 2;
+
+    ml_unique_ptr<vlc_ml_album_list_t> list(vlc_ml_list_genre_albums(p_ml, 
&params, id));
+
+    thumbnailCopy(ml_range_iterate<vlc_ml_album_t>(list), 
std::back_inserter(thumbnails), count);
+
+    return thumbnails;
+}
+
+ThumbnailList extractChildMediaThumbnailsOrIDs(vlc_medialibrary_t *p_ml, const 
int count, const MLItemId &itemID)
+{
+    ThumbnailList result;
+
+    vlc_ml_query_params_t params {};
+
+    // NOTE: We retrieve twice the count to maximize our chances to get a 
valid thumbnail.
+    params.i_nbResults = count * 2;
+
+    ml_unique_ptr<vlc_ml_media_list_t> list(vlc_ml_list_media_of(p_ml, 
&params, itemID.type, itemID.id));
+
+    for (const auto &media : ml_range_iterate<vlc_ml_media_t>(list))
+    {
+        const bool isThumbnailAvailable = 
(media.thumbnails[VLC_ML_THUMBNAIL_SMALL].i_status == 
VLC_ML_THUMBNAIL_STATUS_AVAILABLE);
+        if (isThumbnailAvailable)
+        {
+            
result.existing.push_back(toValidLocalFile(media.thumbnails[VLC_ML_THUMBNAIL_SMALL].psz_mrl));
+        } else if (media.i_type == VLC_ML_MEDIA_TYPE_VIDEO)
+        {
+            result.toGenerate.insert(media.i_id);
+        }
+    }
+
+    if (result.existing.size() > count)
+    {
+        const auto removeStart = result.existing.end() - 
(result.existing.size() - count);
+        result.existing.erase(removeStart, result.existing.end());
+    }
+
+    while (result.toGenerate.size() + result.existing.size() > count)
+    {
+        result.toGenerate.erase(result.toGenerate.begin());
+    }
+
+    return result;
+}
+
+} // anonymous namespace
+
+class CustomCoverImageResponse : public QQuickImageResponse
+{
+public:
+    CustomCoverImageResponse(CoverData data, MediaLib *ml)
+        : ml {ml}
+        , data{data}
+    {
+        // uses Qt::QueuedConnection to give the receiver time to connect to 
finish()
+        QMetaObject::invokeMethod(this, &CustomCoverImageResponse::start, 
Qt::QueuedConnection);
+    }
+
+    QQuickTextureFactory *textureFactory() const override
+    {
+        return !image.isNull() ? 
QQuickTextureFactory::textureFactoryForImage(image) : nullptr;
+    }
+
+private:
+    void start()
+    {
+        const int thumbnailCount = data.countX * data.countY;
+
+        ml->runOnMLThread<ThumbnailList>(this,
+            //ML thread (get child thumbnails or ids)
+            [itemId = data.id, thumbnailCount](vlc_medialibrary_t *p_ml, 
ThumbnailList &ctx)
+            {
+                if (itemId.type == VLC_ML_PARENT_GENRE)
+                    ctx.existing = getGenreMediaThumbnails(p_ml, 
thumbnailCount, itemId.id);
+                else
+                    ctx = extractChildMediaThumbnailsOrIDs(p_ml, 
thumbnailCount, itemId);
+            }
+            //UI Thread
+            , [=](quint64, ThumbnailList & ctx)
+            {
+                if (ctx.toGenerate.empty())
+                {
+                    generateCover(ctx.existing);
+                    return;
+                }
+
+                // request child thumbnail generation, when finished generate 
the cover
+                auto collector = new ThumbnailCollector(this);
+                QObject::connect(collector, &ThumbnailCollector::finished, 
this, [=]()
+                {
+                    const auto thumbnails = ctx.existing + 
collector->allGenerated().values();
+                    generateCover(thumbnails);
+
+                    collector->deleteLater();
+                });
+
+                collector->start(ml, ctx.toGenerate);
+            }
+            );
+    }
+
+    void generateCover(const QStringList &thumbnails)
+    {
+        struct Context { QImage img; };
+
+        ml->runOnMLThread<Context>(this,
+            //ML thread
+            [data = this->data, thumbnails]
+            (vlc_medialibrary_t * , Context & ctx)
+            {
+                CoverGenerator generator;
+                generator.setCountX(data.countX);
+                generator.setCountY(data.countY);
+                generator.setSize(data.size);
+                generator.setSplit((CoverGenerator::Split)data.split);
+                generator.setBlur(data.blur);
+
+                if (!data.defaultCover.isEmpty())
+                    generator.setDefaultThumbnail(data.defaultCover);
+
+                ctx.img = generator.execute(thumbnails);
+            },
+            //UI Thread
+            [this]
+            (quint64, Context & ctx)
+            {
+                doFinish(ctx.img);
+            }
+            );
+    }
+
+    void doFinish(const QImage &result)
+    {
+        image = result;
+        emit finished();
+    }
+
+    MediaLib *ml;
+    CoverData data;
+    QImage image;
+};
+
+
+MLCustomCover::MLCustomCover(const QString &providerId, MediaLib *ml)
+    : m_providerId {providerId}
+    , m_ml {ml}
+{
+}
+
+QString MLCustomCover::get(const MLItemId &parentId, const QSize &size, const 
QString &defaultCover
+                           , const int countX, const int countY, const int 
blur, const bool split_duplicate)
+{
+    QUrl url;
+    url.setScheme(QStringLiteral("image"));
+    url.setHost(m_providerId);
+    url.setQuery(toQuery({parentId, size, countX, countY, blur, 
split_duplicate, defaultCover}));
+    return url.toString();
+}
+
+QQuickImageResponse *MLCustomCover::requestImageResponse(const QString &id, 
const QSize &requestedSize)
+{
+    QString error;
+    CoverData data = fromQuery(QUrlQuery(id), &error);
+    if (!error.isEmpty())
+    {
+        qDebug("failed to parse url %s, error %s", qUtf8Printable(id), 
qUtf8Printable(error));
+        return nullptr;
+    }
+
+    if (requestedSize.isValid())
+        data.size = requestedSize;
+
+    return new CustomCoverImageResponse(data, m_ml);
+}


=====================================
modules/gui/qt/medialibrary/mlitemcover.hpp → 
modules/gui/qt/medialibrary/mlcustomcover.hpp
=====================================
@@ -1,7 +1,7 @@
 /*****************************************************************************
- * Copyright (C) 2021 VLC authors and VideoLAN
+ * Copyright (C) 2022 VLC authors and VideoLAN
  *
- * Authors: Benjamin Arnaud <bun...@omega.gg>
+ * Authors: Prince Gupta <guptaprince8...@gmail.com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -18,35 +18,34 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, 
USA.
  *****************************************************************************/
 
-#ifndef MLITEMCOVER_HPP
-#define MLITEMCOVER_HPP
+#ifndef MLCUSTOMCOVER_HPP
+#define MLCUSTOMCOVER_HPP
 
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
+#include <QQuickAsyncImageProvider>
 
-// Util includes
-#include "util/covergenerator.hpp"
+#include <memory>
 
-// MediaLibrary includes
-#include "mlqmltypes.hpp"
+class MLItemId;
+class MediaLib;
 
-class MLItemCover : public MLItem
+class MLCustomCover : public QQuickAsyncImageProvider
 {
 public:
-    /* explicit */ MLItemCover(const MLItemId & id);
+    MLCustomCover(const QString &providerId, MediaLib *ml);
 
-public: // Interface
-    bool hasGenerator() const;
-    void setGenerator(bool generating);
+    QString get(const MLItemId &parentId
+                , const QSize &size
+                , const QString &defaultCover
+                , const int countX = 2
+                , const int countY = 2
+                , const int blur = 0
+                , const bool split_duplicate = false);
 
-    QString getCover() const;
-    void    setCover(const QString & fileName);
+    QQuickImageResponse *requestImageResponse(const QString &id, const QSize 
&requestedSize);
 
 private:
-    bool m_isGenerating = false;
-
-    QString m_cover;
+    const QString m_providerId;
+    MediaLib *m_ml = nullptr;
 };
 
-#endif
+#endif // MLCUSTOMCOVER_HPP


=====================================
modules/gui/qt/medialibrary/mlfolder.cpp
=====================================
@@ -23,7 +23,7 @@
 // Ctor / dtor
 
 MLFolder::MLFolder(const vlc_ml_folder_t * data)
-    : MLItemCover(MLItemId(data->i_id, VLC_ML_PARENT_FOLDER))
+    : MLItem(MLItemId(data->i_id, VLC_ML_PARENT_FOLDER))
     , m_present(data->b_present)
     , m_banned(data->b_banned)
     , m_title(data->psz_name)


=====================================
modules/gui/qt/medialibrary/mlfolder.hpp
=====================================
@@ -22,9 +22,9 @@
 #define MLFOLDER_HPP
 
 // MediaLibrary includes
-#include "mlitemcover.hpp"
+#include "mlqmltypes.hpp"
 
-class MLFolder : public MLItemCover
+class MLFolder : public MLItem
 {
 public:
     MLFolder(const vlc_ml_folder_t * data);


=====================================
modules/gui/qt/medialibrary/mlgenre.cpp
=====================================
@@ -19,21 +19,12 @@
 #include "mlgenre.hpp"
 
 MLGenre::MLGenre(const vlc_ml_genre_t *_data )
-    : MLItemCover( MLItemId( _data->i_id, VLC_ML_PARENT_GENRE ) )
+    : MLItem( MLItemId( _data->i_id, VLC_ML_PARENT_GENRE ) )
     , m_name     ( QString::fromUtf8( _data->psz_name ) )
     , m_nbTracks ( (unsigned int)_data->i_nb_tracks )
 
 {
     assert(_data);
-
-    for (int i = VLC_ML_THUMBNAIL_SMALL; i < VLC_ML_THUMBNAIL_SIZE_COUNT; ++i)
-    {
-        if (_data->thumbnails[i].psz_mrl)
-        {
-            setCover(_data->thumbnails[i].psz_mrl);
-            break;
-        }
-    }
 }
 
 QString MLGenre::getName() const


=====================================
modules/gui/qt/medialibrary/mlgenre.hpp
=====================================
@@ -24,9 +24,9 @@
 #endif
 
 // MediaLibrary includes
-#include "mlitemcover.hpp"
+#include "mlqmltypes.hpp"
 
-class MLGenre : public MLItemCover
+class MLGenre : public MLItem
 {
 public:
     MLGenre(const vlc_ml_genre_t * _data);


=====================================
modules/gui/qt/medialibrary/mlgenremodel.cpp
=====================================
@@ -22,14 +22,15 @@
 #include "util/covergenerator.hpp"
 
 // MediaLibrary includes
+#include "mlcustomcover.hpp"
 #include "mlartistmodel.hpp"
 
 
//-------------------------------------------------------------------------------------------------
 // Static variables
 
-// NOTE: We multiply by 2 to cover most dpi settings.
-static const int MLGENREMODEL_COVER_WIDTH  = 260 * 2;
-static const int MLGENREMODEL_COVER_HEIGHT = 130 * 2;
+// NOTE: We multiply by 3 to cover most dpi settings.
+static const int MLGENREMODEL_COVER_WIDTH  = 260 * 3;
+static const int MLGENREMODEL_COVER_HEIGHT = 130 * 3;
 
 static const int MLGENREMODEL_COVER_COUNTX = 4;
 static const int MLGENREMODEL_COVER_COUNTY = 2;
@@ -38,29 +39,6 @@ static const int MLGENREMODEL_COVER_BLUR = 4;
 
 
//-------------------------------------------------------------------------------------------------
 
-namespace
-{
-
-QStringList getGenreMediaThumbnails(vlc_medialibrary_t* p_ml, const int count, 
const int64_t id)
-{
-    QStringList thumbnails;
-
-    vlc_ml_query_params_t params;
-
-    memset(&params, 0, sizeof(vlc_ml_query_params_t));
-
-    // NOTE: We retrieve twice the count to maximize our chances to get a 
valid thumbnail.
-    params.i_nbResults = count * 2;
-
-    ml_unique_ptr<vlc_ml_album_list_t> list(vlc_ml_list_genre_albums(p_ml, 
&params, id));
-
-    thumbnailCopy(ml_range_iterate<vlc_ml_album_t>(list), 
std::back_inserter(thumbnails), count);
-
-    return thumbnails;
-}
-
-}
-
 QHash<QByteArray, vlc_ml_sorting_criteria_t> MLGenreModel::M_names_to_criteria 
= {
     {"title", VLC_ML_SORTING_ALPHA}
 };
@@ -161,65 +139,13 @@ vlc_ml_sorting_criteria_t 
MLGenreModel::nameToCriteria(QByteArray name) const
 
 QString MLGenreModel::getCover(MLGenre * genre) const
 {
-    QString cover = genre->getCover();
-
-    // NOTE: Making sure we're not already generating a cover.
-    if (cover.isNull() == false || genre->hasGenerator())
-        return cover;
-
-    MLItemId genreId = genre->getId();
-    struct Context{
-        QString cover;
-    };
-    genre->setGenerator(true);
-    m_mediaLib->runOnMLThread<Context>(this,
-    //ML thread
-    [genreId, coverDefault = m_coverDefault]
-    (vlc_medialibrary_t* ml, Context& ctx)
-    {
-        CoverGenerator generator {genreId};
-
-        generator.setSize(QSize(MLGENREMODEL_COVER_WIDTH,
-                                 MLGENREMODEL_COVER_HEIGHT));
-
-        generator.setCountX(MLGENREMODEL_COVER_COUNTX);
-        generator.setCountY(MLGENREMODEL_COVER_COUNTY);
-
-        generator.setSplit(CoverGenerator::Duplicate);
-
-        generator.setBlur(MLGENREMODEL_COVER_BLUR);
-
-        if (!coverDefault.isEmpty())
-            generator.setDefaultThumbnail(coverDefault);
-
-        if (generator.cachedFileAvailable())
-            ctx.cover = generator.cachedFileURL();
-        else
-            ctx.cover = generator.execute(getGenreMediaThumbnails(ml, 
MLGENREMODEL_COVER_COUNTX * MLGENREMODEL_COVER_COUNTY, genreId.id));
-
-        vlc_ml_media_set_genre_thumbnail(ml, genreId.id, qtu(ctx.cover), 
VLC_ML_THUMBNAIL_SMALL);
-    },
-    //UI thread
-    [this, genreId]
-    (quint64, Context& ctx)
-    {
-        int row = 0;
-        // NOTE: We want to avoid calling 'MLBaseModel::item' for performance 
issues.
-        auto genre = static_cast<MLGenre *>(findInCache(genreId, &row));
-        if (!genre)
-            return;
-
-        genre->setCover(ctx.cover);
-        genre->setGenerator(false);
-
-        //we're running in a callback
-        QModelIndex modelIndex =this->index(row);
-        //we're running in a callback
-        emit const_cast<MLGenreModel*>(this)->dataChanged(modelIndex, 
modelIndex, { GENRE_COVER });
-    });
-
-
-    return cover;
+    return ml()->customCover()->get(genre->getId()
+                                    , QSize(MLGENREMODEL_COVER_WIDTH, 
MLGENREMODEL_COVER_HEIGHT)
+                                    , m_coverDefault
+                                    , MLGENREMODEL_COVER_COUNTX
+                                    , MLGENREMODEL_COVER_COUNTY
+                                    , MLGENREMODEL_COVER_BLUR
+                                    , true);
 }
 
 
//-------------------------------------------------------------------------------------------------


=====================================
modules/gui/qt/medialibrary/mlgroup.cpp
=====================================
@@ -28,7 +28,7 @@
 
//-------------------------------------------------------------------------------------------------
 
 MLGroup::MLGroup(const vlc_ml_group_t * data)
-    : MLItemCover(MLItemId(data->i_id, VLC_ML_PARENT_GROUP))
+    : MLItem(MLItemId(data->i_id, VLC_ML_PARENT_GROUP))
     , m_title(qfu(data->psz_name))
     , m_duration(data->i_duration)
     , m_date(data->i_creation_date)


=====================================
modules/gui/qt/medialibrary/mlgroup.hpp
=====================================
@@ -26,9 +26,9 @@
 #endif
 
 // MediaLibrary includes
-#include "mlitemcover.hpp"
+#include "mlqmltypes.hpp"
 
-class MLGroup : public MLItemCover
+class MLGroup : public MLItem
 {
 public:
     MLGroup(const vlc_ml_group_t * data);


=====================================
modules/gui/qt/medialibrary/mlhelper.cpp
=====================================
@@ -20,43 +20,6 @@
 
 // MediaLibrary includes
 #include "mlbasemodel.hpp"
-#include "mlitemcover.hpp"
-#include "thumbnailcollector.hpp"
-
-namespace
-{
-
-struct ThumbnailList
-{
-    QSet<int64_t> toGenerate;
-    QStringList existing;
-};
-
-ThumbnailList extractChildMediaThumbnailsOrIDs(vlc_medialibrary_t *p_ml, const 
int count, const MLItemId &itemID)
-{
-    ThumbnailList result;
-
-    vlc_ml_query_params_t params {};
-    params.i_nbResults = count;
-
-    ml_unique_ptr<vlc_ml_media_list_t> list(vlc_ml_list_media_of(p_ml, 
&params, itemID.type, itemID.id));
-
-    for (const auto &media : ml_range_iterate<vlc_ml_media_t>(list))
-    {
-        const bool isThumbnailAvailable = 
(media.thumbnails[VLC_ML_THUMBNAIL_SMALL].i_status == 
VLC_ML_THUMBNAIL_STATUS_AVAILABLE);
-        if (isThumbnailAvailable)
-        {
-            
result.existing.push_back(toValidLocalFile(media.thumbnails[VLC_ML_THUMBNAIL_SMALL].psz_mrl));
-        } else if (media.i_type == VLC_ML_MEDIA_TYPE_VIDEO)
-        {
-            result.toGenerate.insert(media.i_id);
-        }
-    }
-
-    return result;
-}
-
-}
 
 QString MsToString( int64_t time , bool doShort )
 {
@@ -83,91 +46,6 @@ QString MsToString( int64_t time , bool doShort )
 
 }
 
-QStringList extractMediaThumbnails(vlc_medialibrary_t *p_ml, const int count, 
const MLItemId &itemID)
-{
-    // NOTE: We retrieve twice the count to maximize our chances to get a 
valid thumbnail.
-    return extractChildMediaThumbnailsOrIDs(p_ml, count * 2, itemID).existing;
-}
-
-QString createGroupMediaCover(const MLBaseModel* model, MLItemCover* parent
-                              , int role
-                              , const std::shared_ptr<CoverGenerator> 
generator)
-{
-    QString cover = parent->getCover();
-
-    // NOTE: Making sure we're not already generating a cover.
-    if (cover.isNull() == false || parent->hasGenerator())
-        return cover;
-
-    if (generator->cachedFileAvailable())
-        return generator->cachedFileURL();
-
-    MLItemId itemId = parent->getId();
-    parent->setGenerator(true);
-
-    const auto generateCover = [=](const QStringList &childCovers)
-    {
-        struct Context { QString cover; };
-
-        model->ml()->runOnMLThread<Context>(model,
-            //ML thread
-            [generator, childCovers]
-            (vlc_medialibrary_t * , Context & ctx)
-            {
-                ctx.cover = generator->execute(childCovers);
-            },
-            //UI Thread
-            [model, itemId, role]
-            (quint64, Context & ctx)
-            {
-                int row;
-
-                // NOTE: We want to avoid calling 'MLBaseModel::item' for 
performance issues.
-                auto item = static_cast<MLItemCover 
*>(model->findInCache(itemId, &row));
-                if (!item)
-                    return;
-
-                item->setCover(ctx.cover);
-                item->setGenerator(false);
-
-                QModelIndex modelIndex = model->index(row);
-                emit const_cast<MLBaseModel *>(model)->dataChanged(modelIndex, 
modelIndex, { role });
-            }
-        );
-    };
-
-    model->ml()->runOnMLThread<ThumbnailList>(model,
-        //ML thread (get child thumbnails or ids)
-        [itemId, generator](vlc_medialibrary_t *p_ml, ThumbnailList &ctx)
-        {
-            ctx = extractChildMediaThumbnailsOrIDs(p_ml, 
generator->requiredNoOfThumbnails(), itemId);
-        }
-        //UI Thread
-        , [=](quint64, ThumbnailList & ctx)
-        {
-            if (ctx.toGenerate.empty())
-            {
-                generateCover(ctx.existing);
-                return;
-            }
-
-            // request child thumbnail generation, when finished generate the 
cover
-            auto collector = new ThumbnailCollector(const_cast<MLBaseModel 
*>(model));
-            QObject::connect(collector, &ThumbnailCollector::finished, model, 
[=]()
-            {
-                const auto thumbnails = ctx.existing + 
collector->allGenerated().values();
-                generateCover(thumbnails);
-
-                collector->deleteLater();
-            });
-
-            collector->start(model->ml(), ctx.toGenerate);
-        }
-    );
-
-    return cover;
-}
-
 QString toValidLocalFile(const char *mrl)
 {
     QUrl url(mrl);


=====================================
modules/gui/qt/medialibrary/mlhelper.hpp
=====================================
@@ -30,7 +30,6 @@
 
 // Forward declarations
 class MLBaseModel;
-class MLItemCover;
 class MLItemId;
 class CoverGenerator;
 
@@ -101,9 +100,4 @@ void thumbnailCopy(const MLListRange<T> &list, O dst, const 
int max)
 
 QString MsToString( int64_t time, bool doShort = false );
 
-QStringList extractMediaThumbnails(vlc_medialibrary_t *p_ml, const int count, 
const MLItemId &itemID);
-
-QString createGroupMediaCover(const MLBaseModel* model, MLItemCover* parent
-                              , int role, const 
std::shared_ptr<CoverGenerator> generator);
-
 #endif // MLHELPER_HPP


=====================================
modules/gui/qt/medialibrary/mlitemcover.cpp deleted
=====================================
@@ -1,55 +0,0 @@
-/*****************************************************************************
- * Copyright (C) 2021 VLC authors and VideoLAN
- *
- * Authors: Benjamin Arnaud <bun...@omega.gg>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * ( at your option ) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, 
USA.
- *****************************************************************************/
-
-#include "mlitemcover.hpp"
-
-//-------------------------------------------------------------------------------------------------
-// Ctor / dtor
-//-------------------------------------------------------------------------------------------------
-
-MLItemCover::MLItemCover(const MLItemId & id)
-    : MLItem(id)
-{}
-
-//-------------------------------------------------------------------------------------------------
-// Interface
-//-------------------------------------------------------------------------------------------------
-
-bool MLItemCover::hasGenerator() const
-{
-    return m_isGenerating;
-}
-
-void MLItemCover::setGenerator(bool generating)
-{
-    m_isGenerating = generating;
-}
-
-//-------------------------------------------------------------------------------------------------
-
-QString MLItemCover::getCover() const
-{
-    return m_cover;
-}
-
-void MLItemCover::setCover(const QString & fileName)
-{
-    m_cover = fileName;
-}


=====================================
modules/gui/qt/medialibrary/mlplaylist.cpp
=====================================
@@ -26,7 +26,7 @@
 
//-------------------------------------------------------------------------------------------------
 
 MLPlaylist::MLPlaylist(const vlc_ml_playlist_t * data)
-    : MLItemCover(MLItemId(data->i_id, VLC_ML_PARENT_PLAYLIST))
+    : MLItem(MLItemId(data->i_id, VLC_ML_PARENT_PLAYLIST))
     , m_name(qfu(data->psz_name))
     , m_duration(0) // TODO m_duration
     , m_count(data->i_nb_media)


=====================================
modules/gui/qt/medialibrary/mlplaylist.hpp
=====================================
@@ -24,9 +24,9 @@
 #endif
 
 // MediaLibrary includes
-#include "mlitemcover.hpp"
+#include "mlqmltypes.hpp"
 
-class MLPlaylist : public MLItemCover
+class MLPlaylist : public MLItem
 {
 public:
     MLPlaylist(const vlc_ml_playlist_t * data);


=====================================
modules/gui/qt/medialibrary/mlplaylistlistmodel.cpp
=====================================
@@ -28,6 +28,7 @@
 
 // MediaLibrary includes
 #include "mlhelper.hpp"
+#include "mlcustomcover.hpp"
 #include "mlplaylist.hpp"
 
 
//-------------------------------------------------------------------------------------------------
@@ -36,9 +37,9 @@
 
 namespace  {
 
-// NOTE: We multiply by 2 to cover most dpi settings.
-const int MLPLAYLISTMODEL_COVER_WIDTH  = 512 * 2; // 16 / 10 ratio
-const int MLPLAYLISTMODEL_COVER_HEIGHT = 320 * 2;
+// NOTE: We multiply by 3 to cover most dpi settings.
+const int MLPLAYLISTMODEL_COVER_WIDTH  = 260 * 3; // 16 / 10 ratio
+const int MLPLAYLISTMODEL_COVER_HEIGHT = 162 * 3;
 
 const int PLAYLIST_COVERX = 2;
 const int PLAYLIST_COVERY = 2;
@@ -300,21 +301,10 @@ void MLPlaylistListModel::endTransaction()
 
 QString MLPlaylistListModel::getCover(MLPlaylist * playlist) const
 {
-    auto generator = std::make_shared<CoverGenerator>(playlist->getId());
-
-    generator->setCountX(PLAYLIST_COVERX);
-    generator->setCountY(PLAYLIST_COVERY);
-
-    generator->setSize(m_coverSize);
-
-    if (!m_coverDefault.isEmpty())
-        generator->setDefaultThumbnail(m_coverDefault);
-
-    generator->setPrefix(m_coverPrefix);
-
-    return createGroupMediaCover(this, playlist
-                                 , PLAYLIST_THUMBNAIL
-                                 , generator);
+    return ml()->customCover()->get(playlist->getId()
+                                    , m_coverSize
+                                    , m_coverDefault
+                                    , PLAYLIST_COVERX, PLAYLIST_COVERY);
 }
 
 
//-------------------------------------------------------------------------------------------------


=====================================
modules/gui/qt/medialibrary/mlvideofoldersmodel.cpp
=====================================
@@ -31,14 +31,14 @@
 #include "util/covergenerator.hpp"
 
 // MediaLibrary includes
-#include "mlhelper.hpp"
+#include "mlcustomcover.hpp"
 #include "mlfolder.hpp"
 
 // Static variables
 
-// NOTE: We multiply by 2 to cover most dpi settings.
-static const int MLVIDEOFOLDERSMODEL_COVER_WIDTH  = 512 * 2; // 16 / 10 ratio
-static const int MLVIDEOFOLDERSMODEL_COVER_HEIGHT = 320 * 2;
+// NOTE: We multiply by 3 to cover most dpi settings.
+static const int MLVIDEOFOLDERSMODEL_COVER_WIDTH  = 260 * 3; // 16 / 10 ratio
+static const int MLVIDEOFOLDERSMODEL_COVER_HEIGHT = 162 * 3;
 
 static const QHash<QByteArray, vlc_ml_sorting_criteria_t> criterias =
 {
@@ -86,11 +86,9 @@ QVariant MLVideoFoldersModel::itemRoleData(MLItem * item, 
const int role) const
             return QVariant::fromValue(folder->getTitle());
         case FOLDER_THUMBNAIL:
         {
-            auto generator = std::make_shared<CoverGenerator>(folder->getId());
-            generator->setSize(QSize(MLVIDEOFOLDERSMODEL_COVER_WIDTH, 
MLVIDEOFOLDERSMODEL_COVER_HEIGHT));
-            generator->setDefaultThumbnail(":/noart_videoCover.svg");
-
-            return createGroupMediaCover(this, folder, FOLDER_THUMBNAIL, 
generator);
+            return ml()->customCover()->get(folder->getId()
+                                            , 
QSize(MLVIDEOFOLDERSMODEL_COVER_WIDTH, MLVIDEOFOLDERSMODEL_COVER_HEIGHT)
+                                            , 
QStringLiteral(":/noart_videoCover.svg"));
         }
         case FOLDER_DURATION:
             return QVariant::fromValue(folder->getDuration());


=====================================
modules/gui/qt/medialibrary/mlvideogroupsmodel.cpp
=====================================
@@ -31,16 +31,16 @@
 #include "util/covergenerator.hpp"
 
 // MediaLibrary includes
-#include "mlhelper.hpp"
+#include "mlcustomcover.hpp"
 #include "mlgroup.hpp"
 #include "mlvideo.hpp"
 
 
//-------------------------------------------------------------------------------------------------
 // Static variables
 
-// NOTE: We multiply by 2 to cover most dpi settings.
-static const int MLVIDEOGROUPSMODEL_COVER_WIDTH  = 512 * 2; // 16 / 10 ratio
-static const int MLVIDEOGROUPSMODEL_COVER_HEIGHT = 320 * 2;
+// NOTE: We multiply by 3 to cover most dpi settings.
+static const int MLVIDEOGROUPSMODEL_COVER_WIDTH  = 260 * 3; // 16 / 10 ratio
+static const int MLVIDEOGROUPSMODEL_COVER_HEIGHT = 162 * 3;
 
 static const QHash<QByteArray, vlc_ml_sorting_criteria_t> criterias =
 {
@@ -93,13 +93,9 @@ QVariant MLVideoGroupsModel::itemRoleData(MLItem * item, 
const int role) const /
                 return QVariant::fromValue(group->getTitle());
             case VIDEO_THUMBNAIL:
             {
-                auto generator = 
std::make_shared<CoverGenerator>(group->getId());
-                generator->setSize(QSize(MLVIDEOGROUPSMODEL_COVER_WIDTH, 
MLVIDEOGROUPSMODEL_COVER_HEIGHT));
-                generator->setDefaultThumbnail(":/noart_videoCover.svg");
-
-                return createGroupMediaCover(this, group
-                                             , VIDEO_THUMBNAIL
-                                             , generator);
+                return ml()->customCover()->get(group->getId()
+                                                , 
QSize(MLVIDEOGROUPSMODEL_COVER_WIDTH, MLVIDEOGROUPSMODEL_COVER_HEIGHT)
+                                                , 
QStringLiteral(":/noart_videoCover.svg"));
             }
             case VIDEO_DURATION:
                 return QVariant::fromValue(group->getDuration());


=====================================
modules/gui/qt/util/covergenerator.cpp
=====================================
@@ -53,12 +53,10 @@ static const QString COVERGENERATOR_DEFAULT = 
":/noart_albumCover.svg";
 // Ctor / dtor
 
//-------------------------------------------------------------------------------------------------
 
-CoverGenerator::CoverGenerator(const MLItemId & itemId)
-    : m_id(itemId)
-    , m_countX(COVERGENERATOR_COUNT)
+CoverGenerator::CoverGenerator()
+    : m_countX(COVERGENERATOR_COUNT)
     , m_countY(COVERGENERATOR_COUNT)
     , m_split(Divide)
-    , m_smooth(true)
     , m_blur(0)
     , m_default(COVERGENERATOR_DEFAULT) {}
 
@@ -66,13 +64,6 @@ CoverGenerator::CoverGenerator(const MLItemId & itemId)
 // Interface
 
//-------------------------------------------------------------------------------------------------
 
-MLItemId CoverGenerator::getId()
-{
-    return m_id;
-}
-
-//-------------------------------------------------------------------------------------------------
-
 void CoverGenerator::setSize(const QSize & size)
 {
     m_size = size;
@@ -93,11 +84,6 @@ void CoverGenerator::setSplit(Split split)
     m_split = split;
 }
 
-void CoverGenerator::setSmooth(bool enabled)
-{
-    m_smooth = enabled;
-}
-
 void CoverGenerator::setBlur(int radius)
 {
     m_blur = radius;
@@ -108,52 +94,17 @@ void CoverGenerator::setDefaultThumbnail(const QString & 
fileName)
     m_default = fileName;
 }
 
-void CoverGenerator::setPrefix(const QString & prefix)
-{
-    m_prefix = prefix;
-}
-
 int CoverGenerator::requiredNoOfThumbnails() const
 {
     return m_countX * m_countY;
 }
 
-bool CoverGenerator::cachedFileAvailable() const
-{
-    return QFile::exists(fileName());
-}
-
-QString CoverGenerator::cachedFileURL() const
-{
-    return QUrl::fromLocalFile(fileName()).toString();
-}
-
-QString CoverGenerator::fileName() const
-{
-    QDir dir(config_GetUserDir(VLC_CACHE_DIR) + COVERGENERATOR_STORAGE);
-    return dir.absoluteFilePath(QString("%1_thumbnail_%2_%3x%4.jpg")
-                                .arg((m_prefix.isEmpty() ? 
getPrefix(m_id.type) : m_prefix)
-                                     , QString::number(m_id.id)
-                                     , QString::number(m_size.width())
-                                     , QString::number(m_size.height())));
-}
-
 
//-------------------------------------------------------------------------------------------------
 // QRunnable implementation
 
//-------------------------------------------------------------------------------------------------
 
-QString CoverGenerator::execute(QStringList thumbnails) const
+QImage CoverGenerator::execute(QStringList thumbnails) const
 {
-    QDir dir(config_GetUserDir(VLC_CACHE_DIR) + COVERGENERATOR_STORAGE);
-
-    dir.mkpath(dir.absolutePath());
-
-    QString fileName = this->fileName();
-    if (dir.exists(fileName))
-    {
-        return QUrl::fromLocalFile(fileName).toString();
-    }
-
     int count = m_countX * m_countY;
 
     int countX;
@@ -216,9 +167,7 @@ QString CoverGenerator::execute(QStringList thumbnails) 
const
     if (m_blur > 0)
         blur(image);
 
-    image.save(fileName, "jpg");
-
-    return QUrl::fromLocalFile(fileName).toString();
+    return image;
 }
 
 
//-------------------------------------------------------------------------------------------------
@@ -230,8 +179,8 @@ void CoverGenerator::draw(QPainter & painter,
 {
     int count = fileNames.count();
 
-    int width  = m_size.width()  / countX;
-    int height = m_size.height() / countY;
+    const int width  = std::ceil(m_size.width()  / 
static_cast<double>(countX));
+    const int height = std::ceil(m_size.height() / 
static_cast<double>(countY));
 
     for (int y = 0; y < countY; y++)
     {
@@ -285,35 +234,11 @@ void CoverGenerator::drawImage(QPainter & painter, const 
QString & fileName, con
     QSize size = reader.size().scaled(target.width(),
                                       target.height(), 
Qt::KeepAspectRatioByExpanding);
 
-    QImage image;
-
-    if (fileName.endsWith(".svg", Qt::CaseInsensitive))
-    {
-        if (size.isEmpty() == false)
-        {
-            reader.setScaledSize(size);
-        }
-
-        if (reader.read(&image) == false)
-            return;
-    }
-    else
-    {
-        if (reader.read(&image) == false)
-            return;
-
-        if (size.isEmpty() == false)
-        {
-            // NOTE: Should we use Qt::SmoothTransformation or favor 
efficiency ?
-            if (m_smooth)
-                image = image.scaled(size, Qt::IgnoreAspectRatio, 
Qt::SmoothTransformation);
-            else
-                image = image.scaled(size, Qt::IgnoreAspectRatio);
-        }
-    }
+    reader.setScaledSize(size);
+    QImage image = reader.read();
 
-    int x = (image.width () - target.width ()) / 2;
-    int y = (image.height() - target.height()) / 2;
+    int x = std::ceil((image.width() - target.width()) / 2.);
+    int y = std::ceil((image.height() - target.height()) / 2.);
 
     QRect source(x, y, target.width(), target.height());
 


=====================================
modules/gui/qt/util/covergenerator.hpp
=====================================
@@ -48,7 +48,7 @@ public: // Enums
     };
 
 public:
-    CoverGenerator(const MLItemId & itemId);
+    CoverGenerator();
 
 public: // Interface
     MLItemId getId();
@@ -69,16 +69,9 @@ public: // Interface
 
     void setDefaultThumbnail(const QString & fileName);
 
-    // NOTE: This lets us enforce a specific prefix for the cover fileName.
-    void setPrefix(const QString & prefix);
-
     int requiredNoOfThumbnails() const;
 
-    bool cachedFileAvailable() const;
-
-    QString cachedFileURL() const;
-
-    QString execute(QStringList thumbnails) const;
+    QImage execute(QStringList thumbnails) const;
 
 private: // Functions
     QString fileName() const;
@@ -92,8 +85,6 @@ private: // Functions
     QString getPrefix(vlc_ml_parent_type type) const;
 
 private:
-    MLItemId m_id;
-
     QSize m_size;
 
     int m_countX;
@@ -101,13 +92,9 @@ private:
 
     Split m_split;
 
-    bool m_smooth;
-
     int m_blur;
 
     QString m_default;
-
-    QString m_prefix;
 };
 
 #endif // COVERGENERATOR_HPP


=====================================
modules/gui/qt/widgets/native/roundimage.cpp
=====================================
@@ -31,10 +31,12 @@
 
 #include <QBuffer>
 #include <QCache>
+#include <QFile>
 #include <QImage>
 #include <QImageReader>
 #include <QPainter>
 #include <QPainterPath>
+#include <QQuickImageProvider>
 #include <QQuickWindow>
 #include <QGuiApplication>
 #include <QSGImageNode>
@@ -72,49 +74,228 @@ namespace
     }
 
     // images are cached (result of RoundImageGenerator) with the cost 
calculated from QImage::sizeInBytes
-    QCache<ImageCacheKey, QImage> imageCache(2 * 1024 * 1024); // 2 MiB
+    QCache<ImageCacheKey, QImage> imageCache(32 * 1024 * 1024); // 32 MiB
 
-    std::unique_ptr<QIODevice> getReadable(const QUrl &url)
-    try
+    QRectF doPreserveAspectCrop(const QSizeF &sourceSize, const QSizeF &size)
     {
-        if (!QQmlFile::isLocalFile(url))
+        const qreal ratio = std::max(size.width() / sourceSize.width(), 
size.height() / sourceSize.height());
+        const QSizeF imageSize = sourceSize * ratio;
+        const QPointF alignedCenteredTopLeft {(size.width() - 
imageSize.width()) / 2., (size.height() - imageSize.height()) / 2.};
+        return {alignedCenteredTopLeft, imageSize};
+    }
+
+    class ImageReader : public AsyncTask<QImage>
+    {
+    public:
+        // requestedSize is only taken as hint, the Image is resized with 
PreserveAspectCrop
+        ImageReader(QIODevice *device, QSize requestedSize)
+            : device {device}
+            , requestedSize {requestedSize}
+        {
+        }
+
+        QString errorString() const { return errorStr; }
+
+        QImage execute()
+        {
+            QImageReader reader;
+            reader.setDevice(device);
+            const QSize sourceSize = reader.size();
+
+            if (requestedSize.isValid())
+                reader.setScaledSize(doPreserveAspectCrop(sourceSize, 
requestedSize).size().toSize());
+
+            auto img = reader.read();
+            errorStr = reader.errorString();
+            return img;
+        }
+
+    private:
+        QIODevice *device;
+        QSize requestedSize;
+        QString errorStr;
+    };
+
+    class LocalImageResponse : public QQuickImageResponse
+    {
+    public:
+        LocalImageResponse(const QString &fileName, const QSize &requestedSize)
+        {
+            auto file = new QFile(fileName);
+            reader.reset(new ImageReader(file, requestedSize));
+            file->setParent(reader.get());
+
+            connect(reader.get(), &ImageReader::result, this, 
&LocalImageResponse::handleImageRead);
+
+            reader->start(*QThreadPool::globalInstance());
+        }
+
+        QQuickTextureFactory *textureFactory() const override
+        {
+            return result.isNull() ? nullptr : 
QQuickTextureFactory::textureFactoryForImage(result);
+        }
+
+        QString errorString() const override
+        {
+            return errorStr;
+        }
+
+    private:
+        void handleImageRead()
         {
+            result = reader->takeResult();
+            errorStr = reader->errorString();
+            reader.reset();
+
+            emit finished();
+        }
+
+        QImage result;
+        TaskHandle<ImageReader> reader;
+        QString errorStr;
+    };
+
 #ifdef QT_NETWORK_LIB
-            QNetworkAccessManager networkMgr;
-            
networkMgr.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
-            auto reply = networkMgr.get(QNetworkRequest(url));
-            QEventLoop loop;
-            QObject::connect(reply, &QNetworkReply::finished, &loop, 
&QEventLoop::quit);
-            loop.exec();
+    class NetworkImageResponse : public QQuickImageResponse
+    {
+    public:
+        NetworkImageResponse(QNetworkReply *reply, QSize requestedSize) : 
reply {reply}, requestedSize {requestedSize}
+        {
+            QObject::connect(reply, &QNetworkReply::finished
+                             , this, 
&NetworkImageResponse::handleNetworkReplyFinished);
+        }
+
+        QQuickTextureFactory *textureFactory() const override
+        {
+            return result.isNull() ? nullptr : 
QQuickTextureFactory::textureFactoryForImage(result);
+        }
+
+        QString errorString() const override
+        {
+            return error;
+        }
+
+        void cancel() override
+        {
+            if (reply->isRunning())
+                reply->abort();
+
+            reader.reset();
+        }
 
+    private:
+        void handleNetworkReplyFinished()
+        {
             if (reply->error() != QNetworkReply::NoError)
-                throw std::runtime_error(reply->errorString().toStdString());
+            {
+                error = reply->errorString();
+                emit finished();
+                return;
+            }
 
-            class DataOwningBuffer : private QByteArray, public QBuffer
+            reader.reset(new ImageReader(reply, requestedSize));
+            QObject::connect(reader.get(), &ImageReader::result, this, [this]()
             {
-            public:
-                explicit DataOwningBuffer(const QByteArray &data)
-                    : QByteArray(data), QBuffer(this, nullptr) { }
-            };
-
-            auto file = std::make_unique<DataOwningBuffer>(reply->readAll());
-            file->open(QIODevice::ReadOnly);
-            return file;
-#else
-            throw std::runtime_error("Qt Network Library is not available!");
+                result = reader->takeResult();
+                error = reader->errorString();
+                reader.reset();
+
+                emit finished();
+            });
+
+            reader->start(*QThreadPool::globalInstance());
+        }
+
+        QNetworkReply *reply;
+        QSize requestedSize;
+        TaskHandle<ImageReader> reader;
+        QImage result;
+        QString error;
+    };
 #endif
+
+    class ImageProviderAsyncAdaptor : public QQuickImageResponse
+    {
+    public:
+        ImageProviderAsyncAdaptor(QQuickImageProvider *provider, const QString 
&id, const QSize &requestedSize)
+        {
+            task.reset(new ProviderImageGetter(provider, id, requestedSize));
+            connect(task.get(), &ProviderImageGetter::result, this, [this]()
+            {
+                result = task->takeResult();
+                task.reset();
+
+                emit finished();
+            });
+
+            task->start(*QThreadPool::globalInstance());
         }
-        else
+
+        QQuickTextureFactory *textureFactory() const override
         {
-            auto file = 
std::make_unique<QFile>(QQmlFile::urlToLocalFileOrQrc(url));
-            file->open(QIODevice::ReadOnly);
-            return file;
+            return result.isNull() ? nullptr : 
QQuickTextureFactory::textureFactoryForImage(result);
         }
-    }
-    catch (const std::exception& error)
+
+    private:
+        class ProviderImageGetter : public AsyncTask<QImage>
+        {
+        public:
+            ProviderImageGetter(QQuickImageProvider *provider, const QString 
&id, const QSize &requestedSize)
+                : provider {provider}
+                , id{id}
+                , requestedSize{requestedSize}
+            {
+            }
+
+            QImage execute() override
+            {
+                return provider->requestImage(id, &sourceSize, requestedSize);
+            }
+
+        private:
+            QQuickImageProvider *provider;
+            QString id;
+            QSize requestedSize;
+            QSize sourceSize;
+        };
+
+        TaskHandle<ProviderImageGetter> task;
+        QImage result;
+    };
+
+    QQuickImageResponse *getAsyncImageResponse(const QUrl &url, const QSize 
&requestedSize, QQmlEngine *engine)
     {
-        qWarning() << "Could not load source image:" << url << error.what();
-        return {};
+        if (url.scheme() == QStringLiteral("image"))
+        {
+            auto provider = engine->imageProvider(url.host());
+            if (!provider)
+                return nullptr;
+
+            assert(provider->imageType() == QQmlImageProviderBase::Image
+                   || provider->imageType() == 
QQmlImageProviderBase::ImageResponse);
+
+            const auto imageId = url.toString(QUrl::RemoveScheme | 
QUrl::RemoveAuthority).mid(1);;
+
+            if (provider->imageType() == QQmlImageProviderBase::Image)
+                return new 
ImageProviderAsyncAdaptor(static_cast<QQuickImageProvider *>(provider), 
imageId, requestedSize);
+            if (provider->imageType() == QQmlImageProviderBase::ImageResponse)
+                return static_cast<QQuickAsyncImageProvider 
*>(provider)->requestImageResponse(imageId, requestedSize);
+
+            return nullptr;
+        }
+        else if (QQmlFile::isLocalFile(url))
+        {
+            return new LocalImageResponse(QQmlFile::urlToLocalFileOrQrc(url), 
requestedSize);
+        }
+#ifdef QT_NETWORK_LIB
+        else
+        {
+            QNetworkRequest request(url);
+            request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, 
QNetworkRequest::NoLessSafeRedirectPolicy);
+            auto reply = engine->networkAccessManager()->get(request);
+            return new NetworkImageResponse(reply, requestedSize);
+        }
+#endif
     }
 }
 
@@ -127,6 +308,11 @@ RoundImage::RoundImage(QQuickItem *parent) : QQuickItem 
{parent}
     connect(this, &QQuickItem::widthChanged, this, 
&RoundImage::regenerateRoundImage);
 }
 
+RoundImage::~RoundImage()
+{
+    resetImageRequest();
+}
+
 QSGNode *RoundImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
 {
     auto node = static_cast<QSGImageNode *>(oldNode);
@@ -229,114 +415,134 @@ void RoundImage::setDPR(const qreal value)
     regenerateRoundImage();
 }
 
-void RoundImage::regenerateRoundImage()
+void RoundImage::handleImageRequestFinished()
 {
-    if (!isComponentComplete() || m_enqueuedGeneration)
-        return;
+    const QString error = m_activeImageRequest->errorString();
+    QImage image;
+    if (auto textureFactory = m_activeImageRequest->textureFactory())
+    {
+        image = textureFactory->image();
+        delete textureFactory;
+    }
 
-    // remove old contents
-    m_dirty = true;
-    m_roundImage = {};
-    update();
-    setFlag(ItemHasContents, false); // update() is still required
+    resetImageRequest();
 
-    m_roundImageGenerator.reset();
+    if (image.isNull())
+    {
+        qDebug() << "failed to get image, error" << error << source();
+        return;
+    }
 
-    // use Qt::QueuedConnection to delay generation, so that dependent 
properties
-    // subsequent updates can be merged, f.e when VLCStyle.scale changes
-    m_enqueuedGeneration = true;
+    const qreal scaledWidth = this->width() * m_dpr;
+    const qreal scaledHeight = this->height() * m_dpr;
+    const qreal scaledRadius = this->radius() * m_dpr;
+
+    const ImageCacheKey key {source(), QSizeF {scaledWidth, 
scaledHeight}.toSize(), scaledRadius};
 
-    QMetaObject::invokeMethod(this, [this] ()
+    // Image is generated in size factor of `m_dpr` to avoid scaling artefacts 
when
+    // generated image is set with device pixel ratio
+    m_roundImageGenerator.reset(new RoundImageGenerator(image, scaledWidth, 
scaledHeight, scaledRadius));
+    connect(m_roundImageGenerator.get(), &BaseAsyncTask::result, this, [this, 
key]()
     {
-        m_enqueuedGeneration = false;
-        assert(!m_roundImageGenerator);
+        const auto image = new QImage(m_roundImageGenerator->takeResult());
 
-        const qreal scaledWidth = this->width() * m_dpr;
-        const qreal scaledHeight = this->height() * m_dpr;
-        const qreal scaledRadius = this->radius() * m_dpr;
+        m_roundImageGenerator.reset();
 
-        const ImageCacheKey key {source(), QSizeF {scaledWidth, 
scaledHeight}.toSize(), scaledRadius};
-        if (auto image = imageCache.object(key)) // should only by called in 
mainthread
+        if (image->isNull())
         {
-            m_roundImage = *image;
-            m_dirty = true;
-            setFlag(ItemHasContents, true);
-            update();
+            delete image;
+            setRoundImage({});
             return;
         }
 
-        // Image is generated in size factor of `m_dpr` to avoid scaling 
artefacts when
-        // generated image is set with device pixel ratio
-        m_roundImageGenerator.reset(new RoundImageGenerator(m_source, 
scaledWidth, scaledHeight, scaledRadius));
-        connect(m_roundImageGenerator.get(), &BaseAsyncTask::result, this, 
[this, key]()
-        {
-            const auto image = new QImage(m_roundImageGenerator->takeResult());
+        image->setDevicePixelRatio(m_dpr);
+        setRoundImage(*image);
 
-            m_roundImageGenerator.reset();
+        imageCache.insert(key, image, image->sizeInBytes());
+    });
 
-            if (!image->isNull())
-            {
-                image->setDevicePixelRatio(m_dpr);
+    m_roundImageGenerator->start(*QThreadPool::globalInstance());
+}
 
-                imageCache.insert(key, image, image->sizeInBytes());
+void RoundImage::resetImageRequest()
+{
+    if (!m_activeImageRequest)
+        return;
 
-                setFlag(ItemHasContents, true);
+    m_activeImageRequest->disconnect(this);
+    m_activeImageRequest->deleteLater();
+    m_activeImageRequest = nullptr;
+}
 
-                m_roundImage = *image;
+void RoundImage::load()
+{
+    m_enqueuedGeneration = false;
+    assert(!m_roundImageGenerator);
 
-                m_dirty = true;
-            }
-            else
-            {
-                delete image;
-                m_dirty = false;
-                setFlag(ItemHasContents, false);
-            }
+    auto engine = qmlEngine(this);
+    if (!engine || m_source.isEmpty() || !size().isValid() || size().isEmpty())
+        return;
+
+    const qreal scaledWidth = this->width() * m_dpr;
+    const qreal scaledHeight = this->height() * m_dpr;
+    const qreal scaledRadius = this->radius() * m_dpr;
 
-            update();
-        });
+    const ImageCacheKey key {source(), QSizeF {scaledWidth, 
scaledHeight}.toSize(), scaledRadius};
+    if (auto image = imageCache.object(key)) // should only by called in 
mainthread
+    {
+        setRoundImage(*image);
+        return;
+    }
 
-        m_roundImageGenerator->start(*QThreadPool::globalInstance());
-    }, Qt::QueuedConnection);
+    m_activeImageRequest = getAsyncImageResponse(source(), QSizeF 
{scaledWidth, scaledHeight}.toSize(), engine);
+    connect(m_activeImageRequest, &QQuickImageResponse::finished, this, 
&RoundImage::handleImageRequestFinished);
 }
 
-RoundImage::RoundImageGenerator::RoundImageGenerator(const QUrl &source, qreal 
width, qreal height, qreal radius)
-    : source(source)
-    , width(width)
-    , height(height)
-    , radius(radius)
+void RoundImage::setRoundImage(QImage image)
 {
+    m_dirty = true;
+    m_roundImage = image;
+
+    // remove old contents, setting ItemHasContent to false will
+    // inhibit updatePaintNode() call and old content will remain
+    if (image.isNull())
+        update();
+
+    setFlag(ItemHasContents, not image.isNull());
+    update();
 }
 
-QImage RoundImage::RoundImageGenerator::execute()
+void RoundImage::regenerateRoundImage()
 {
-    if (width <= 0 || height <= 0)
-        return {};
+    if (!isComponentComplete() || m_enqueuedGeneration)
+        return;
 
-    if (source.isEmpty())
-        return {};
+    // remove old contents
+    setRoundImage({});
 
-    auto file = getReadable(source);
-    if (!file || !file->isOpen())
-        return {};
+    resetImageRequest();
 
-    QImageReader sourceReader(file.get());
+    m_roundImageGenerator.reset();
 
-    // do PreserveAspectCrop
-    const QSizeF size {width, height};
-    QSizeF defaultSize = sourceReader.size();
-    if (!defaultSize.isValid())
-        defaultSize = size;
+    // use Qt::QueuedConnection to delay generation, so that dependent 
properties
+    // subsequent updates can be merged, f.e when VLCStyle.scale changes
+    m_enqueuedGeneration = true;
 
-    const qreal ratio = std::max(size.width() / defaultSize.width(), 
size.height() / defaultSize.height());
-    const QSizeF targetSize = defaultSize * ratio;
-    const QPointF alignedCenteredTopLeft {(size.width() - targetSize.width()) 
/ 2., (size.height() - targetSize.height()) / 2.};
-    sourceReader.setScaledSize(targetSize.toSize());
+    QMetaObject::invokeMethod(this, &RoundImage::load, Qt::QueuedConnection);
+}
 
-    if (Q_UNLIKELY(radius <= 0))
-    {
-        return sourceReader.read();
-    }
+RoundImage::RoundImageGenerator::RoundImageGenerator(const QImage 
&sourceImage, qreal width, qreal height, qreal radius)
+    : sourceImage(sourceImage)
+    , width(width)
+    , height(height)
+    , radius(radius)
+{
+}
+
+QImage RoundImage::RoundImageGenerator::execute()
+{
+    if (width <= 0 || height <= 0 || sourceImage.isNull())
+        return {};
 
     QImage target(width, height, QImage::Format_ARGB32_Premultiplied);
     if (target.isNull())
@@ -353,7 +559,10 @@ QImage RoundImage::RoundImageGenerator::execute()
         path.addRoundedRect(0, 0, width, height, radius, radius);
         painter.setClipPath(path);
 
-        painter.drawImage({alignedCenteredTopLeft, targetSize}, 
sourceReader.read());
+        // do PreserveAspectCrop
+        const auto imageSize = sourceImage.size();
+        const QPointF alignedCenteredTopLeft {(width - imageSize.width()) / 
2., (height - imageSize.height()) / 2.};
+        painter.drawImage(QRectF {alignedCenteredTopLeft, imageSize}, 
sourceImage);
     }
 
     return target;


=====================================
modules/gui/qt/widgets/native/roundimage.hpp
=====================================
@@ -31,6 +31,8 @@
 #include <QQuickItem>
 #include <QUrl>
 
+class QQuickImageResponse;
+
 class RoundImage : public QQuickItem
 {
     Q_OBJECT
@@ -42,6 +44,7 @@ class RoundImage : public QQuickItem
 
 public:
     RoundImage(QQuickItem *parent = nullptr);
+    ~RoundImage();
 
     void componentComplete() override;
 
@@ -64,18 +67,22 @@ private:
     class RoundImageGenerator : public AsyncTask<QImage>
     {
     public:
-        RoundImageGenerator(const QUrl &source, qreal width, qreal height, 
qreal radius);
+        RoundImageGenerator(const QImage &sourceImage, qreal width, qreal 
height, qreal radius);
 
         QImage execute();
 
     private:
-        QUrl source;
+        QImage sourceImage;
         qreal width;
         qreal height;
         qreal radius;
     };
 
     void setDPR(qreal value);
+    void handleImageRequestFinished();
+    void resetImageRequest();
+    void load();
+    void setRoundImage(QImage image);
     void regenerateRoundImage();
 
     QUrl m_source;
@@ -86,6 +93,7 @@ private:
     bool m_dirty = false;
 
     TaskHandle<RoundImageGenerator> m_roundImageGenerator {};
+    QQuickImageResponse *m_activeImageRequest {};
 
     bool m_enqueuedGeneration = false;
 };


=====================================
po/POTFILES.in
=====================================
@@ -783,8 +783,6 @@ modules/gui/qt/medialibrary/mlfoldersmodel.cpp
 modules/gui/qt/medialibrary/mlfoldersmodel.hpp
 modules/gui/qt/medialibrary/mlgroup.cpp
 modules/gui/qt/medialibrary/mlgroup.hpp
-modules/gui/qt/medialibrary/mlitemcover.cpp
-modules/gui/qt/medialibrary/mlitemcover.hpp
 modules/gui/qt/medialibrary/mlplaylistlistmodel.cpp
 modules/gui/qt/medialibrary/mlplaylistlistmodel.hpp
 modules/gui/qt/medialibrary/mlplaylistmedia.cpp



View it on GitLab: 
https://code.videolan.org/videolan/vlc/-/compare/a00c5e0744a169ef73abae92293157a5aeb377b9...716ff4b79ee52227010ff2f39ca61cb6316923c9

-- 
View it on GitLab: 
https://code.videolan.org/videolan/vlc/-/compare/a00c5e0744a169ef73abae92293157a5aeb377b9...716ff4b79ee52227010ff2f39ca61cb6316923c9
You're receiving this email because of your account on code.videolan.org.


VideoLAN code repository instance
_______________________________________________
vlc-commits mailing list
vlc-commits@videolan.org
https://mailman.videolan.org/listinfo/vlc-commits

Reply via email to