Hugo Beauzée-Luyssen pushed to branch master at VideoLAN / VLC
Commits: 80b1d991 by Benjamin Arnaud at 2021-05-08T10:59:52+00:00 qt/mlbasemodel: Add the 'itemCache' function This is useful when we only want to return a cached item. - - - - - 22bc7af6 by Benjamin Arnaud at 2021-05-08T10:59:52+00:00 qt: Create CoverGenerator This class can be useful to retrieve composed thumbnails for groups, playlists and genres. It supports a few options like custom size, count and division type. - - - - - 74ffcb26 by Benjamin Arnaud at 2021-05-08T10:59:52+00:00 qt/mlgroup: Add CoverGenerator implementation - - - - - e6ebc5f8 by Benjamin Arnaud at 2021-05-08T10:59:52+00:00 qt/mlgrouplistmodel: Add CoverGenerator implementation - - - - - 73072c14 by Benjamin Arnaud at 2021-05-08T10:59:52+00:00 qt/mlgenre: Add CoverGenerator implementation - - - - - 43e3ea1e by Benjamin Arnaud at 2021-05-08T10:59:52+00:00 qt/mlgenremodel: Add CoverGenerator implementation - - - - - 4362af46 by Benjamin Arnaud at 2021-05-08T10:59:52+00:00 qml/MusicGenres: Add gradient overlay fix #25595 - - - - - 16 changed files: - modules/gui/qt/Makefile.am - modules/gui/qt/maininterface/mainui.cpp - modules/gui/qt/medialibrary/mlbasemodel.cpp - modules/gui/qt/medialibrary/mlbasemodel.hpp - modules/gui/qt/medialibrary/mlgenre.cpp - modules/gui/qt/medialibrary/mlgenre.hpp - modules/gui/qt/medialibrary/mlgenremodel.cpp - modules/gui/qt/medialibrary/mlgenremodel.hpp - modules/gui/qt/medialibrary/mlgroup.cpp - modules/gui/qt/medialibrary/mlgroup.hpp - modules/gui/qt/medialibrary/mlgrouplistmodel.cpp - modules/gui/qt/medialibrary/mlgrouplistmodel.hpp - modules/gui/qt/medialibrary/qml/MusicGenres.qml - + modules/gui/qt/util/covergenerator.cpp - + modules/gui/qt/util/covergenerator.hpp - po/POTFILES.in Changes: ===================================== modules/gui/qt/Makefile.am ===================================== @@ -218,6 +218,8 @@ libqt_plugin_la_SOURCES = \ gui/qt/util/audio_device_model.cpp \ gui/qt/util/audio_device_model.hpp \ gui/qt/util/color_scheme_model.cpp gui/qt/util/color_scheme_model.hpp \ + gui/qt/util/covergenerator.cpp \ + gui/qt/util/covergenerator.hpp \ gui/qt/util/imageluminanceextractor.cpp gui/qt/util/imageluminanceextractor.hpp \ gui/qt/util/imagehelper.cpp gui/qt/util/imagehelper.hpp \ gui/qt/util/i18n.cpp gui/qt/util/i18n.hpp \ @@ -343,9 +345,7 @@ nodist_libqt_plugin_la_SOURCES = \ gui/qt/medialibrary/mlartistmodel.moc.cpp \ gui/qt/medialibrary/mlbasemodel.moc.cpp \ gui/qt/medialibrary/mlfoldersmodel.moc.cpp \ - gui/qt/medialibrary/mlgenre.moc.cpp \ gui/qt/medialibrary/mlgenremodel.moc.cpp \ - gui/qt/medialibrary/mlgroup.moc.cpp \ gui/qt/medialibrary/mlgrouplistmodel.moc.cpp \ gui/qt/medialibrary/mlqmltypes.moc.cpp \ gui/qt/medialibrary/mlrecentsmodel.moc.cpp \ @@ -374,6 +374,7 @@ nodist_libqt_plugin_la_SOURCES = \ gui/qt/util/asynctask.moc.cpp \ gui/qt/util/audio_device_model.moc.cpp \ gui/qt/util/color_scheme_model.moc.cpp \ + gui/qt/util/covergenerator.moc.cpp \ gui/qt/util/imageluminanceextractor.moc.cpp \ gui/qt/util/i18n.moc.cpp \ gui/qt/util/listcache.moc.cpp \ ===================================== modules/gui/qt/maininterface/mainui.cpp ===================================== @@ -199,7 +199,6 @@ void MainUI::registerQMLTypes() registerAnonymousType<MLAlbum>("org.videolan.medialib", 1); registerAnonymousType<MLArtist>("org.videolan.medialib", 1); registerAnonymousType<MLAlbumTrack>("org.videolan.medialib", 1); - registerAnonymousType<MLGenre>("org.videolan.medialib", 1); registerAnonymousType<MLPlaylist>("org.videolan.medialib", 1); qmlRegisterType<AlbumContextMenu>( "org.videolan.medialib", 0, 1, "AlbumContextMenu" ); ===================================== modules/gui/qt/medialibrary/mlbasemodel.cpp ===================================== @@ -408,19 +408,37 @@ void MLBaseModel::invalidateCache() m_cache.reset(); } +//------------------------------------------------------------------------------------------------- + MLItem *MLBaseModel::item(int signedidx) const { validateCache(); ssize_t count = m_cache->count(); - if (count == COUNT_UNINITIALIZED || signedidx < 0 - || signedidx >= count) + + if (count == COUNT_UNINITIALIZED || signedidx < 0 || signedidx >= count) return nullptr; unsigned int idx = static_cast<unsigned int>(signedidx); + m_cache->refer(idx); const std::unique_ptr<MLItem> *item = m_cache->get(idx); + + if (!item) + /* Not in cache */ + return nullptr; + + /* Return raw pointer */ + return item->get(); +} + +MLItem *MLBaseModel::itemCache(int signedidx) const +{ + unsigned int idx = static_cast<unsigned int>(signedidx); + + const std::unique_ptr<MLItem> *item = m_cache->get(idx); + if (!item) /* Not in cache */ return nullptr; @@ -429,6 +447,8 @@ MLItem *MLBaseModel::item(int signedidx) const return item->get(); } +//------------------------------------------------------------------------------------------------- + MLBaseModel::BaseLoader::BaseLoader(vlc_medialibrary_t *ml, MLItemId parent, QString searchPattern, vlc_ml_sorting_criteria_t sort, bool sort_desc) : m_ml(ml) ===================================== modules/gui/qt/medialibrary/mlbasemodel.hpp ===================================== @@ -107,7 +107,12 @@ protected: void validateCache() const; void invalidateCache(); - MLItem* item(int signedidx) const; + + MLItem *item(int signedidx) const; + + // NOTE: This is faster because it only returns items available in cache. + MLItem *itemCache(int signedidx) const; + virtual void onVlcMlEvent( const MLEvent &event ); virtual ListCacheLoader<std::unique_ptr<MLItem>> *createLoader() const = 0; ===================================== modules/gui/qt/medialibrary/mlgenre.cpp ===================================== @@ -16,220 +16,27 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. *****************************************************************************/ -#include <cassert> -#include <QPainter> -#include <QImage> -#include <QThreadPool> -#include <QMutex> -#include <QWaitCondition> -#include <QDir> -#include <QGradient> -#include <QGraphicsScene> -#include <QGraphicsPixmapItem> -#include <QGraphicsBlurEffect> -#include <algorithm> #include "mlgenre.hpp" -#include "qt.hpp" -namespace { - -#define THUMBNAIL_WIDTH 260 -#define THUMBNAIL_HEIGHT 130 - -QImage blurImage(const QImage& src) -{ - QGraphicsScene scene; - QGraphicsPixmapItem item; - item.setPixmap(QPixmap::fromImage(src)); - QGraphicsBlurEffect blurEffect; - blurEffect.setBlurRadius(4); - blurEffect.setBlurHints(QGraphicsBlurEffect::QualityHint); - item.setGraphicsEffect(&blurEffect); - scene.addItem(&item); - QImage res(src.size(), QImage::Format_ARGB32); - QPainter ptr(&res); - scene.render(&ptr); - return res; -} +MLGenre::MLGenre(vlc_medialibrary_t* ml, const vlc_ml_genre_t *_data ) + : MLItem ( MLItemId( _data->i_id, VLC_ML_PARENT_GENRE ) ) + , m_ml ( ml ) + , m_generator( nullptr ) + , m_name ( QString::fromUtf8( _data->psz_name ) ) + , m_nbTracks ( (unsigned int)_data->i_nb_tracks ) -class GenerateCoverTask : public QRunnable { -public: - GenerateCoverTask(vlc_medialibrary_t* ml, MLGenre* genre, QString filepath) - : QRunnable() - , m_ml(ml) - , m_genre(genre) - , m_filepath(filepath) - { - } - - void drawRegion(QPainter& target, QString source, const QRect& rect) - { - QImage tmpImage; - if (tmpImage.load(source)) - { - QRect sourceRect; - int size = std::min(tmpImage.width(), tmpImage.height()); - if (rect.width() == rect.height()) - { - sourceRect = QRect( (tmpImage.width() - size) / 2, - (tmpImage.height() - size) / 2, - size, - size); - } - else if (rect.width() > rect.height()) - { - sourceRect = QRect( (tmpImage.width() - size) / 2, - (tmpImage.height() - size/2) / 2, - size, - size/2); - } - else - { - sourceRect = QRect( (tmpImage.width() - size / 2) / 2, - (tmpImage.height() - size) / 2, - size/2, - size); - } - target.drawImage(rect, tmpImage, sourceRect); - } - else - { - target.setPen(Qt::black); - target.drawRect(rect); - } - } - - void run() override - { - { - QMutexLocker lock(&m_taskLock); - if (m_canceled) { - m_taskCond.wakeAll(); - return; - } - m_running = true; - } - - int64_t genreId = m_genre->getId().id; - ml_unique_ptr<vlc_ml_album_list_t> album_list; - //TODO only retreive albums with a cover. - vlc_ml_query_params_t queryParams; - memset(&queryParams, 0, sizeof(vlc_ml_query_params_t)); - album_list.reset( vlc_ml_list_genre_albums(m_ml, &queryParams, genreId) ); - - QStringList thumbnails; - thumbnails.reserve(8); - for( const vlc_ml_album_t& media: ml_range_iterate<vlc_ml_album_t>( album_list ) ) { - if (media.thumbnails[VLC_ML_THUMBNAIL_SMALL].i_status == - VLC_ML_THUMBNAIL_STATUS_AVAILABLE) { - QUrl mediaURL( media.thumbnails[VLC_ML_THUMBNAIL_SMALL].psz_mrl ); - //QImage only accept local file - if (mediaURL.isValid() && mediaURL.isLocalFile()) { - thumbnails.append(mediaURL.path()); - if (thumbnails.size() == 8) - break; - } - } - } - - if (thumbnails.empty()) { - thumbnails.append(":/noart_album.svg"); - } - - assert(thumbnails.size() <= 8); - std::copy(thumbnails.begin(), ( thumbnails.begin() + ( 8 - thumbnails.size() ) ), std::back_inserter(thumbnails)); - assert(thumbnails.size() == 8); - - { - QMutexLocker lock(&m_taskLock); - if (m_canceled) { - m_running = false; - m_taskCond.wakeAll(); - return; - } - } - - QImage image(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, QImage::Format_RGB32); - image.fill(Qt::white); - - QPainter painter; - painter.begin(&image); - for (int i = 0; i < 2; i++) { - for (int j = 0; j < 4; j++) { - drawRegion(painter, thumbnails[2*j+1], QRect( ( THUMBNAIL_WIDTH / 4 ) * j, ( THUMBNAIL_HEIGHT / 2 ) * i, THUMBNAIL_WIDTH / 4, THUMBNAIL_HEIGHT / 2 )); - } - } - painter.end(); - - image = blurImage(image); - - QLinearGradient gradient; - gradient.setColorAt(0, QColor(0, 0, 0, 255*.3)); - gradient.setColorAt(1, QColor(0, 0, 0, 255*.7)); - painter.begin(&image); - painter.setOpacity(.7); - painter.fillRect(image.rect(), gradient); - painter.end(); - - if (image.save(m_filepath, "jpg")) - /* Set the cover from the main thread */ - QMetaObject::invokeMethod(m_genre, [genre = m_genre, cover = QUrl::fromLocalFile(m_filepath).toString()] - { - genre->setCover(std::move(cover)); - }); - - { - QMutexLocker lock(&m_taskLock); - m_running = false; - m_taskCond.wakeAll(); - } - } - - void cancel() - { - QMutexLocker lock(&m_taskLock); - m_canceled = true; - if (!m_running) - return; - m_taskCond.wait(&m_taskLock); - } - -private: - bool m_canceled = false; - bool m_running = false; - QMutex m_taskLock; - QWaitCondition m_taskCond; - - vlc_medialibrary_t* m_ml = nullptr; - MLGenre* m_genre = nullptr; - QString m_filepath; -}; - + assert(_data); } - -MLGenre::MLGenre(vlc_medialibrary_t* ml, const vlc_ml_genre_t *_data, QObject *_parent ) - : QObject(_parent) - , MLItem ( MLItemId( _data->i_id, VLC_ML_PARENT_GENRE ) ) - , m_ml ( ml ) - , m_name ( QString::fromUtf8( _data->psz_name ) ) - , m_nbTracks ( (unsigned int)_data->i_nb_tracks ) - +bool MLGenre::hasGenerator() const { - assert(_data); - connect(this, &MLGenre::askGenerateCover, this, &MLGenre::generateThumbnail); + return m_generator.get(); } -MLGenre::~MLGenre() +void MLGenre::setGenerator(CoverGenerator * generator) { - if (m_coverTask) { - if (!QThreadPool::globalInstance()->tryTake(m_coverTask)) { - //task is done or running - static_cast<GenerateCoverTask*>(m_coverTask)->cancel(); - } - delete m_coverTask; - } + m_generator.reset(generator); } QString MLGenre::getName() const @@ -244,43 +51,11 @@ unsigned int MLGenre::getNbTracks() const QString MLGenre::getCover() const { - if (!m_cover.isEmpty()) - return m_cover; - if (!m_coverTask) { - emit askGenerateCover( QPrivateSignal() ); - } return m_cover; } -void MLGenre::setCover(QString cover) -{ - m_cover = cover; - //TODO store in media library -} - -void MLGenre::generateThumbnail() +void MLGenre::setCover(const QString & fileName) { - if (!m_coverTask && m_cover.isNull()) { - - QDir dir(config_GetUserDir(VLC_CACHE_DIR)); - dir.mkdir("art"); - dir.cd("art"); - dir.mkdir("qt-genre-covers"); - dir.cd("qt-genre-covers"); - - QString filename = QString("genre_thumbnail_%1.jpg").arg(getId().id); - QString absoluteFilePath = dir.absoluteFilePath(filename); - if (dir.exists(filename)) - { - setCover(QUrl::fromLocalFile(absoluteFilePath).toString()); - } - else - { - GenerateCoverTask* coverTask = new GenerateCoverTask(m_ml, this, absoluteFilePath); - coverTask->setAutoDelete(false); - m_coverTask = coverTask; - QThreadPool::globalInstance()->start(coverTask); - } - } + m_cover = fileName; } ===================================== modules/gui/qt/medialibrary/mlgenre.hpp ===================================== @@ -22,33 +22,26 @@ #ifdef HAVE_CONFIG_H #include "config.h" #endif -#include "vlc_common.h" -#include <memory> -#include <QObject> -#include <QString> -#include <QList> -#include <QRunnable> -#include <vlc_media_library.h> -#include "mlhelper.hpp" +// Util includes +#include "util/covergenerator.hpp" + +// MediaLibrary includes #include "mlqmltypes.hpp" -class MLGenre : public QObject, public MLItem +class MLGenre : public MLItem { - Q_OBJECT - public: - MLGenre( vlc_medialibrary_t* _ml, const vlc_ml_genre_t *_data, QObject *_parent = nullptr); - ~MLGenre(); + MLGenre( vlc_medialibrary_t* _ml, const vlc_ml_genre_t *_data ); + + bool hasGenerator() const; + void setGenerator(CoverGenerator * generator); QString getName() const; unsigned int getNbTracks() const; - QString getCover() const; - void setCover(QString cover); - -signals: - void askGenerateCover( QPrivateSignal ) const; + QString getCover() const; + void setCover(const QString & fileName); private slots: void generateThumbnail(); @@ -56,9 +49,11 @@ private slots: private: vlc_medialibrary_t* m_ml; + TaskHandle<CoverGenerator> m_generator; + QString m_name; QString m_cover; - QRunnable* m_coverTask = nullptr; + unsigned int m_nbTracks; }; ===================================== modules/gui/qt/medialibrary/mlgenremodel.cpp ===================================== @@ -18,8 +18,25 @@ #include "mlgenremodel.hpp" +// Util includes +#include "util/covergenerator.hpp" + +// MediaLibrary includes #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; + +static const int MLGENREMODEL_COVER_COUNTX = 4; +static const int MLGENREMODEL_COVER_COUNTY = 2; + +static const int MLGENREMODEL_COVER_BLUR = 4; + +//------------------------------------------------------------------------------------------------- QHash<QByteArray, vlc_ml_sorting_criteria_t> MLGenreModel::M_names_to_criteria = { {"title", VLC_ML_SORTING_ALPHA} @@ -32,10 +49,12 @@ MLGenreModel::MLGenreModel(QObject *parent) QVariant MLGenreModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() < 0) + int row = index.row(); + + if (!index.isValid() || row < 0) return QVariant(); - const MLGenre* ml_genre = static_cast<MLGenre *>(item(index.row())); + MLGenre* ml_genre = static_cast<MLGenre *>(item(row)); if (!ml_genre) return QVariant(); @@ -49,7 +68,7 @@ QVariant MLGenreModel::data(const QModelIndex &index, int role) const case GENRE_NB_TRACKS: return QVariant::fromValue( ml_genre->getNbTracks() ); case GENRE_COVER: - return QVariant::fromValue( ml_genre->getCover() ); + return getCover(ml_genre, row); default : return QVariant(); } @@ -102,6 +121,72 @@ vlc_ml_sorting_criteria_t MLGenreModel::nameToCriteria(QByteArray name) const return M_names_to_criteria.value(name, VLC_ML_SORTING_DEFAULT); } +QString MLGenreModel::getCover(MLGenre * genre, int index) const +{ + QString cover = genre->getCover(); + + // NOTE: Making sure we're not already generating a cover. + if (cover.isNull() == false || genre->hasGenerator()) + return cover; + + CoverGenerator * generator = new CoverGenerator(m_ml, genre->getId(), index); + + 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); + + generator->setDefaultThumbnail(":/noart_album.svg"); + + // NOTE: We'll apply the new cover once it's loaded. + connect(generator, &CoverGenerator::result, this, &MLGenreModel::onCover); + + generator->start(*QThreadPool::globalInstance()); + + genre->setGenerator(generator); + + return cover; +} + +//------------------------------------------------------------------------------------------------- +// Private slots +//------------------------------------------------------------------------------------------------- + +void MLGenreModel::onCover() +{ + CoverGenerator * generator = static_cast<CoverGenerator *> (sender()); + + int index = generator->getIndex(); + + // NOTE: We want to avoid calling 'MLBaseModel::item' for performance issues. + MLItem * item = this->itemCache(index); + + // NOTE: When the item is no longer cached or has been moved we return right away. + if (item == nullptr || item->getId() != generator->getId()) + { + generator->deleteLater(); + + return; + } + + MLGenre * genre = static_cast<MLGenre *> (item); + + QString fileName = QUrl::fromLocalFile(generator->takeResult()).toString(); + + genre->setCover(fileName); + + genre->setGenerator(nullptr); + + thumbnailUpdated(index); +} + +//------------------------------------------------------------------------------------------------- + ListCacheLoader<std::unique_ptr<MLItem>> * MLGenreModel::createLoader() const { ===================================== modules/gui/qt/medialibrary/mlgenremodel.hpp ===================================== @@ -60,15 +60,21 @@ private: vlc_ml_sorting_criteria_t roleToCriteria(int role) const override; vlc_ml_sorting_criteria_t nameToCriteria(QByteArray name) const override; + QString getCover(MLGenre * genre, int index) const; - static QHash<QByteArray, vlc_ml_sorting_criteria_t> M_names_to_criteria; +private slots: + void onCover(); +private: struct Loader : public BaseLoader { Loader(const MLGenreModel &model) : BaseLoader(model) {} size_t count() const override; std::vector<std::unique_ptr<MLItem>> load(size_t index, size_t count) const override; }; + +private: // Variables + static QHash<QByteArray, vlc_ml_sorting_criteria_t> M_names_to_criteria; }; ===================================== modules/gui/qt/medialibrary/mlgroup.cpp ===================================== @@ -21,17 +21,16 @@ #include "mlgroup.hpp" // VLC includes -#include <vlc_media_library.h> #include "qt.hpp" //------------------------------------------------------------------------------------------------- // Ctor / dtor //------------------------------------------------------------------------------------------------- -MLGroup::MLGroup(vlc_medialibrary_t * ml, const vlc_ml_group_t * data, QObject * parent) - : QObject(parent) - , MLItem(MLItemId(data->i_id, VLC_ML_PARENT_GROUP)) +MLGroup::MLGroup(vlc_medialibrary_t * ml, const vlc_ml_group_t * data) + : MLItem(MLItemId(data->i_id, VLC_ML_PARENT_GROUP)) , m_ml(ml) + , m_generator(nullptr) , m_name(qfu(data->psz_name)) , m_duration(data->i_duration) , m_date(data->i_creation_date) @@ -44,16 +43,37 @@ MLGroup::MLGroup(vlc_medialibrary_t * ml, const vlc_ml_group_t * data, QObject * // Interface //------------------------------------------------------------------------------------------------- +bool MLGroup::hasGenerator() const +{ + return m_generator.get(); +} + +void MLGroup::setGenerator(CoverGenerator * generator) +{ + m_generator.reset(generator); +} + +//------------------------------------------------------------------------------------------------- + QString MLGroup::getName() const { return m_name; } -QString MLGroup::getThumbnail() +//------------------------------------------------------------------------------------------------- + +QString MLGroup::getCover() const { - return QString(); + return m_cover; } +void MLGroup::setCover(const QString & fileName) +{ + m_cover = fileName; +} + +//------------------------------------------------------------------------------------------------- + int64_t MLGroup::getDuration() const { return m_duration; ===================================== modules/gui/qt/medialibrary/mlgroup.hpp ===================================== @@ -25,23 +25,25 @@ #include "config.h" #endif +// Util includes +#include "util/covergenerator.hpp" + // MediaLibrary includes #include "mlqmltypes.hpp" -// Qt includes -#include <QObject> - -class MLGroup : public QObject, public MLItem +class MLGroup : public MLItem { - Q_OBJECT - public: - MLGroup(vlc_medialibrary_t * ml, const vlc_ml_group_t * data, QObject * parent = nullptr); + MLGroup(vlc_medialibrary_t * ml, const vlc_ml_group_t * data); public: // Interface + bool hasGenerator() const; + void setGenerator(CoverGenerator * generator); + QString getName() const; - QString getThumbnail(); + QString getCover() const; + void setCover(const QString & fileName); int64_t getDuration() const; @@ -52,8 +54,12 @@ public: // Interface private: vlc_medialibrary_t * m_ml; + TaskHandle<CoverGenerator> m_generator; + QString m_name; + QString m_cover; + int64_t m_duration; unsigned int m_date; ===================================== modules/gui/qt/medialibrary/mlgrouplistmodel.cpp ===================================== @@ -27,6 +27,9 @@ // VLC includes #include <vlc_media_library.h> +// Util includes +#include "util/covergenerator.hpp" + // MediaLibrary includes #include "mlhelper.hpp" #include "mlgroup.hpp" @@ -35,6 +38,10 @@ //------------------------------------------------------------------------------------------------- // Static variables +// NOTE: We multiply by 2 to cover most dpi settings. +static const int MLGROUPLISTMODEL_COVER_WIDTH = 512 * 2; // 16 / 10 ratio +static const int MLGROUPLISTMODEL_COVER_HEIGHT = 320 * 2; + static const QHash<QByteArray, vlc_ml_sorting_criteria_t> criterias = { {"id", VLC_ML_SORTING_DEFAULT}, @@ -85,8 +92,9 @@ QHash<int, QByteArray> MLGroupListModel::roleNames() const /* override */ QVariant MLGroupListModel::data(const QModelIndex & index, int role) const /* override */ { + int row = index.row(); - MLItem * item = this->item(index.row()); + MLItem * item = this->item(row); if (item == nullptr) return QVariant(); @@ -109,7 +117,7 @@ QVariant MLGroupListModel::data(const QModelIndex & index, int role) const /* ov case GROUP_NAME: return QVariant::fromValue(group->getName()); case GROUP_THUMBNAIL: - return QVariant::fromValue(group->getThumbnail()); + return getCover(group, row); case GROUP_DURATION: return QVariant::fromValue(group->getDuration()); case GROUP_DATE: @@ -207,6 +215,35 @@ ListCacheLoader<std::unique_ptr<MLItem>> * MLGroupListModel::createLoader() cons return new Loader(*this); } +//------------------------------------------------------------------------------------------------- +// Private functions +//------------------------------------------------------------------------------------------------- + +QString MLGroupListModel::getCover(MLGroup * group, int index) const +{ + QString cover = group->getCover(); + + // NOTE: Making sure we're not already generating a cover. + if (cover.isNull() == false || group->hasGenerator()) + return cover; + + CoverGenerator * generator = new CoverGenerator(m_ml, group->getId(), index); + + generator->setSize(QSize(MLGROUPLISTMODEL_COVER_WIDTH, + MLGROUPLISTMODEL_COVER_HEIGHT)); + + generator->setDefaultThumbnail(":/noart_videoCover.svg"); + + // NOTE: We'll apply the new thumbnail once it's loaded. + connect(generator, &CoverGenerator::result, this, &MLGroupListModel::onCover); + + generator->start(*QThreadPool::globalInstance()); + + group->setGenerator(generator); + + return cover; +} + //------------------------------------------------------------------------------------------------- // Private MLBaseModel reimplementation //------------------------------------------------------------------------------------------------- @@ -230,7 +267,41 @@ void MLGroupListModel::onVlcMlEvent(const MLEvent & event) /* override */ void MLGroupListModel::thumbnailUpdated(int idx) /* override */ { - emit dataChanged(index(idx), index(idx), { GROUP_THUMBNAIL }); + QModelIndex index = this->index(idx); + + emit dataChanged(index, index, { GROUP_THUMBNAIL }); +} + +//------------------------------------------------------------------------------------------------- +// Private slots +//------------------------------------------------------------------------------------------------- + +void MLGroupListModel::onCover() +{ + CoverGenerator * generator = static_cast<CoverGenerator *> (sender()); + + int index = generator->getIndex(); + + // NOTE: We want to avoid calling 'MLBaseModel::item' for performance issues. + MLItem * item = this->itemCache(index); + + // NOTE: When the item is no longer cached or has been moved we return right away. + if (item == nullptr || item->getId() != generator->getId()) + { + generator->deleteLater(); + + return; + } + + MLGroup * group = static_cast<MLGroup *> (item); + + QString fileName = QUrl::fromLocalFile(generator->takeResult()).toString(); + + group->setCover(fileName); + + group->setGenerator(nullptr); + + thumbnailUpdated(index); } //================================================================================================= ===================================== modules/gui/qt/medialibrary/mlgrouplistmodel.hpp ===================================== @@ -26,6 +26,7 @@ // Forward declarations class vlc_medialibrary_t; +class MLGroup; class MLGroupListModel : public MLBaseModel { @@ -72,11 +73,17 @@ protected: // MLBaseModel implementation ListCacheLoader<std::unique_ptr<MLItem>> * createLoader() const override; +private: // Functions + QString getCover(MLGroup * group, int index) const; + private: // MLBaseModel implementation void onVlcMlEvent(const MLEvent & event) override; void thumbnailUpdated(int idx) override; +private slots: + void onCover(); + private: struct Loader : public MLBaseModel::BaseLoader { ===================================== modules/gui/qt/medialibrary/qml/MusicGenres.qml ===================================== @@ -187,6 +187,18 @@ Widgets.NavigableFocusScope { } pictureOverlay: Item { + Rectangle + { + anchors.fill: parent + + radius: VLCStyle.gridCover_radius + + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.3) } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.7) } + } + } + Column { anchors.centerIn: parent ===================================== modules/gui/qt/util/covergenerator.cpp ===================================== @@ -0,0 +1,379 @@ +/***************************************************************************** + * 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 "covergenerator.hpp" + +// VLC includes +#include "qt.hpp" + +// MediaLibrary includes +#include "medialibrary/mlhelper.hpp" + +// Qt includes +#include <QDir> +#include <QGraphicsScene> +#include <QGraphicsPixmapItem> +#include <QGraphicsBlurEffect> + +//------------------------------------------------------------------------------------------------- +// Static variables + +static const QString COVERGENERATOR_STORAGE = "/art/qt-covers"; + +static const int COVERGENERATOR_COUNT = 2; + +static const QString COVERGENERATOR_DEFAULT = ":/noart.png"; + +//------------------------------------------------------------------------------------------------- +// Ctor / dtor +//------------------------------------------------------------------------------------------------- + +CoverGenerator::CoverGenerator(vlc_medialibrary_t * ml, const MLItemId & itemId, int index) + : m_ml(ml) + , m_id(itemId) + , m_index(index) + , m_countX(COVERGENERATOR_COUNT) + , m_countY(COVERGENERATOR_COUNT) + , m_split(Divide) + , m_smooth(false) + , m_blur(0) + , m_default(COVERGENERATOR_DEFAULT) {} + +//------------------------------------------------------------------------------------------------- +// Interface +//------------------------------------------------------------------------------------------------- + +/* Q_INVOKABLE */ MLItemId CoverGenerator::getId() +{ + return m_id; +} + +/* Q_INVOKABLE */ int CoverGenerator::getIndex() +{ + return m_index; +} + +//------------------------------------------------------------------------------------------------- + +/* Q_INVOKABLE */ void CoverGenerator::setSize(const QSize & size) +{ + m_size = size; +} + +/* Q_INVOKABLE */ void CoverGenerator::setCountX(int x) +{ + m_countX = x; +} + +/* Q_INVOKABLE */ void CoverGenerator::setCountY(int y) +{ + m_countY = y; +} + +/* Q_INVOKABLE */ void CoverGenerator::setSplit(Split split) +{ + m_split = split; +} + +/* Q_INVOKABLE */ void CoverGenerator::setSmooth(bool enabled) +{ + m_smooth = enabled; +} + +/* Q_INVOKABLE */ void CoverGenerator::setBlur(int radius) +{ + m_blur = radius; +} + +/* Q_INVOKABLE */ void CoverGenerator::setDefaultThumbnail(const QString & fileName) +{ + m_default = fileName; +} + +//------------------------------------------------------------------------------------------------- +// QRunnable implementation +//------------------------------------------------------------------------------------------------- + +QString CoverGenerator::execute() /* override */ +{ + QDir dir(config_GetUserDir(VLC_CACHE_DIR) + COVERGENERATOR_STORAGE); + + dir.mkpath(dir.absolutePath()); + + vlc_ml_parent_type type = m_id.type; + + int64_t id = m_id.id; + + QString string = getStringType(type); + + QString fileName = QString("%1_thumbnail_%2.jpg").arg(string).arg(id); + + fileName = dir.absoluteFilePath(fileName); + + if (dir.exists(fileName)) + { + return fileName; + } + + QStringList thumbnails; + + int count = m_countX * m_countY; + + if (type == VLC_ML_PARENT_GENRE) + thumbnails = getGenre(count, id); + else + thumbnails = getMedias(count, id, type); + + if (thumbnails.isEmpty()) + { + if (m_split == CoverGenerator::Duplicate) + { + while (thumbnails.count() != count) + { + thumbnails.append(m_default); + } + } + else + { + thumbnails.append(m_default); + + m_countX = 1; + m_countY = 1; + } + } + else if (m_split == CoverGenerator::Duplicate) + { + int index = 0; + + while (thumbnails.count() != count) + { + thumbnails.append(thumbnails.at(index)); + + index++; + } + } + else // if (m_split == CoverGenerator::Divide) + { + // NOTE: This handles the 2x2 case. + if (thumbnails.count() == 2) + { + m_countX = 2; + m_countY = 1; + } + } + + QImage image(m_size, QImage::Format_RGB32); + + image.fill(Qt::white); + + QPainter painter; + + painter.begin(&image); + + draw(painter, thumbnails); + + painter.end(); + + if (m_blur > 0) + blur(&image); + + image.save(fileName, "jpg"); + + return fileName; +} + +//------------------------------------------------------------------------------------------------- +// Private functions +//------------------------------------------------------------------------------------------------- + +void CoverGenerator::draw(QPainter & painter, const QStringList & fileNames) +{ + int count = fileNames.count(); + + int width = m_size.width() / m_countX; + int height = m_size.height() / m_countY; + + for (int y = 0; y < m_countY; y++) + { + for (int x = 0; x < m_countX; x++) + { + int index = m_countX * y + x; + + if (index == count) return; + + QRect rect; + + // NOTE: This handles the wider thumbnail case (e.g. for a 2x1 grid). + if (index == count - 1 && x != m_countX - 1) + { + rect = QRect(width * x, height * y, width * m_countX - x, height); + } + else + rect = QRect(width * x, height * y, width, height); + + drawImage(painter, fileNames.at(index), rect); + } + } +} + +void CoverGenerator::drawImage(QPainter & painter, const QString & fileName, const QRect & target) +{ + QImage image; + + if (fileName.isEmpty()) + image.load(m_default); + else + image.load(fileName); + + // NOTE: This image does not seem valid so we paint the placeholder instead. + if (image.isNull()) + { + image.load(m_default); + } + + // NOTE: Should we use Qt::SmoothTransformation or favor efficiency ? + if (m_smooth) + image = image.scaled(target.size(), Qt::KeepAspectRatioByExpanding, + Qt::SmoothTransformation); + else + image = image.scaled(target.size(), Qt::KeepAspectRatioByExpanding); + + int x = (image.width () - target.width ()) / 2; + int y = (image.height() - target.height()) / 2; + + QRect source(x, y, target.width(), target.height()); + + painter.drawImage(target, image, source); +} + +//------------------------------------------------------------------------------------------------- + +// FIXME: This implementation is not ideal and uses a dedicated QGraphicsScene. +void CoverGenerator::blur(QImage * image) +{ + assert(image); + + QGraphicsScene scene; + + QGraphicsPixmapItem item(QPixmap::fromImage(*image)); + + QGraphicsBlurEffect effect; + + effect.setBlurRadius(m_blur); + + effect.setBlurHints(QGraphicsBlurEffect::QualityHint); + + item.setGraphicsEffect(&effect); + + scene.addItem(&item); + + QImage result(image->size(), QImage::Format_ARGB32); + + QPainter painter(&result); + + scene.render(&painter); + + *image = result; +} + +//------------------------------------------------------------------------------------------------- + +QString CoverGenerator::getStringType(vlc_ml_parent_type type) const +{ + switch (type) + { + case VLC_ML_PARENT_GENRE: + return "genre"; + case VLC_ML_PARENT_GROUP: + return "group"; + case VLC_ML_PARENT_PLAYLIST: + return "playlist"; + default: + return "unknown"; + } +} + +//------------------------------------------------------------------------------------------------- + +QStringList CoverGenerator::getGenre(int count, int64_t id) const +{ + QStringList thumbnails; + + vlc_ml_query_params_t params; + + memset(¶ms, 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(m_ml, ¶ms, id)); + + for (const vlc_ml_album_t & album : ml_range_iterate<vlc_ml_album_t>(list)) + { + if (album.thumbnails[VLC_ML_THUMBNAIL_SMALL].i_status != VLC_ML_THUMBNAIL_STATUS_AVAILABLE) + continue; + + QUrl url(album.thumbnails[VLC_ML_THUMBNAIL_SMALL].psz_mrl); + + // NOTE: We only want local files to compose the cover. + if (url.isLocalFile() == false) + continue; + + thumbnails.append(url.path()); + + if (thumbnails.count() == count) + return thumbnails; + } + + return thumbnails; +} + +QStringList CoverGenerator::getMedias(int count, int64_t id, vlc_ml_parent_type type) const +{ + QStringList thumbnails; + + vlc_ml_query_params_t params; + + memset(¶ms, 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_media_list_t> list(vlc_ml_list_media_of(m_ml, ¶ms, type, id)); + + for (const vlc_ml_media_t & media : ml_range_iterate<vlc_ml_media_t>(list)) + { + if (media.thumbnails[VLC_ML_THUMBNAIL_SMALL].i_status != VLC_ML_THUMBNAIL_STATUS_AVAILABLE) + continue; + + QUrl url(media.thumbnails[VLC_ML_THUMBNAIL_SMALL].psz_mrl); + + // NOTE: We only want local files to compose the cover. + if (url.isLocalFile() == false) + continue; + + thumbnails.append(url.path()); + + if (thumbnails.count() == count) + return thumbnails; + } + + return thumbnails; +} ===================================== modules/gui/qt/util/covergenerator.hpp ===================================== @@ -0,0 +1,114 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +#ifndef COVERGENERATOR_HPP +#define COVERGENERATOR_HPP + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +// MediaLibrary includes +#include "medialibrary/mlqmltypes.hpp" + +// Util includes +#include "util/asynctask.hpp" + +// Qt includes +#include <QPainter> + +// Forward declarations +class vlc_medialibrary_t; +class MLItemId; + +class CoverGenerator : public AsyncTask<QString> +{ + Q_OBJECT + + Q_ENUMS(Split) + +public: // Enums + enum Split + { + Divide, + Duplicate + }; + +public: + CoverGenerator(vlc_medialibrary_t * ml, const MLItemId & itemId, int index = -1); + +public: // Interface + Q_INVOKABLE MLItemId getId(); + + Q_INVOKABLE int getIndex(); + + Q_INVOKABLE void setSize(const QSize & size); + + Q_INVOKABLE void setCountX(int x); + Q_INVOKABLE void setCountY(int y); + + // NOTE: Do we want to divide or duplicate thumbnails to reach the proper count ? + Q_INVOKABLE void setSplit(Split split); + + // NOTE: Applies SmoothTransformation to thumbnails. Disabled by default. + Q_INVOKABLE void setSmooth(bool enabled); + + // NOTE: You need to specify a radius to enable blur, 8 looks good. + Q_INVOKABLE void setBlur(int radius); + + Q_INVOKABLE void setDefaultThumbnail(const QString & fileName); + +public: // AsyncTask implementation + QString execute() override; + +private: // Functions + void draw(QPainter & painter, const QStringList & fileNames); + + void drawImage(QPainter & painter, const QString & fileName, const QRect & rect); + + void blur(QImage * image); + + QString getStringType(vlc_ml_parent_type type) const; + + QStringList getMedias(int count, int64_t id, vlc_ml_parent_type type) const; + QStringList getGenre (int count, int64_t id) const; + +private: + vlc_medialibrary_t * m_ml; + + MLItemId m_id; + + int m_index; + + QSize m_size; + + int m_countX; + int m_countY; + + Split m_split; + + bool m_smooth; + + int m_blur; + + QString m_default; +}; + +#endif // COVERGENERATOR_HPP ===================================== po/POTFILES.in ===================================== @@ -791,6 +791,8 @@ modules/gui/qt/widgets/native/animators.cpp modules/gui/qt/widgets/native/animators.hpp modules/gui/qt/widgets/native/customwidgets.cpp modules/gui/qt/widgets/native/customwidgets.hpp +modules/gui/qt/util/covergenerator.cpp +modules/gui/qt/util/covergenerator.hpp modules/gui/qt/util/imagehelper.cpp modules/gui/qt/util/imagehelper.hpp modules/gui/qt/util/qt_dirs.cpp View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/12d1b4f87cdd89cc9a4d325c1303445ac94abf03...4362af465dc8f395cfdb3cd2e67a8992c1b67111 -- View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/12d1b4f87cdd89cc9a4d325c1303445ac94abf03...4362af465dc8f395cfdb3cd2e67a8992c1b67111 You're receiving this email because of your account on code.videolan.org.
_______________________________________________ vlc-commits mailing list vlc-commits@videolan.org https://mailman.videolan.org/listinfo/vlc-commits