From 6cf142a44a96b393600c9d504f658428651c45f0 Mon Sep 17 00:00:00 2001 From: Digital Artifex <7929434+DigitalArtifex@users.noreply.github.com> Date: Sun, 21 Sep 2025 06:57:40 -0400 Subject: [PATCH] Added Pexels Media API --- plugin/PexelsImageMetadata.h | 25 +++ plugin/PexelsImageSearch.cpp | 386 +++++++++++++++++++++++++++++++++++ plugin/PexelsImageSearch.h | 216 ++++++++++++++++++++ plugin/PexelsVideoMetadata.h | 63 ++++++ plugin/PexelsVideoModel.cpp | 293 ++++++++++++++++++++++++++ plugin/PexelsVideoModel.h | 149 ++++++++++++++ plugin/PexelsVideoSearch.cpp | 383 ++++++++++++++++++++++++++++++++++ plugin/PexelsVideoSearch.h | 196 ++++++++++++++++++ 8 files changed, 1711 insertions(+) create mode 100644 plugin/PexelsImageMetadata.h create mode 100644 plugin/PexelsImageSearch.cpp create mode 100644 plugin/PexelsImageSearch.h create mode 100644 plugin/PexelsVideoMetadata.h create mode 100644 plugin/PexelsVideoModel.cpp create mode 100644 plugin/PexelsVideoModel.h create mode 100644 plugin/PexelsVideoSearch.cpp create mode 100644 plugin/PexelsVideoSearch.h diff --git a/plugin/PexelsImageMetadata.h b/plugin/PexelsImageMetadata.h new file mode 100644 index 0000000..9eb2d47 --- /dev/null +++ b/plugin/PexelsImageMetadata.h @@ -0,0 +1,25 @@ +#ifndef PEXELSIMAGEMETADATA_H +#define PEXELSIMAGEMETADATA_H + +#include +#include + +#include "Komplex_global.h" + +struct KOMPLEX_EXPORT PexelsImageMetadata +{ + QString alt; + QString averageColorCode; + quint64 height = 0; + quint64 id = 0; + bool liked = false; + QString photographer; + QUrl photographerUrl; + quint64 photographerId = 0; + QMap sources; + QUrl thumbnail; + QUrl url; + quint64 width = 0; +}; + +#endif // PEXELSIMAGEMETADATA_H diff --git a/plugin/PexelsImageSearch.cpp b/plugin/PexelsImageSearch.cpp new file mode 100644 index 0000000..e1027ac --- /dev/null +++ b/plugin/PexelsImageSearch.cpp @@ -0,0 +1,386 @@ +#include "PexelsImageSearch.h" +#include "PexelsAPI.h" + +PexelsImageSearchModel::PexelsImageSearchModel(QObject *parent) : QAbstractItemModel { parent } +{ + m_networkManager.setAutoDeleteReplies(true); +} + +PexelsImageSearchModel::~PexelsImageSearchModel() +{ + +} + +int PexelsImageSearchModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_data.size(); +} + +QVariant PexelsImageSearchModel::data(const QModelIndex &index, int role) const +{ + if(index.row() < 0 || index.row() >= m_data.count()) + return QVariant(); + + QVariant data; + + switch (static_cast(role)) { + case Alt: + data = QVariant::fromValue(m_data[index.row()].alt); + break; + case AverageColor: + data = QVariant::fromValue(m_data[index.row()].averageColorCode); + break; + case Height: + data = QVariant::fromValue(m_data[index.row()].height); + break; + case Id: + data = QVariant::fromValue(m_data[index.row()].id); + break; + case Liked: + data = QVariant::fromValue(m_data[index.row()].liked); + break; + case Photographer: + data = QVariant::fromValue(m_data[index.row()].photographer); + break; + case PhotographerUrl: + data = QVariant::fromValue(m_data[index.row()].photographerUrl); + break; + case PhotographerId: + data = QVariant::fromValue(m_data[index.row()].photographerId); + break; + case Thumbnail: + data = QVariant::fromValue(m_data[index.row()].thumbnail); + break; + case Url: + data = QVariant::fromValue(m_data[index.row()].url); + break; + case Width: + data = QVariant::fromValue(m_data[index.row()].width); + break; + case Original: + case Large2x: + case Large: + case Medium: + case Small: + case Portrait: + case Landscape: + if(m_data[index.row()].sources.contains(QString::fromUtf8(m_dataRoles[role]))) + data = QVariant::fromValue(m_data[index.row()].sources[QString::fromUtf8(m_dataRoles[role])]); + break; + } + + return data; +} + +QHash PexelsImageSearchModel::roleNames() const +{ + return m_dataRoles; +} + +void PexelsImageSearchModel::getSearchResults(QString url) +{ + setStatus(Searching); + + QNetworkRequest request; + request.setRawHeader(QStringLiteral("Authorization").toLatin1(), QStringLiteral(PAK).toLatin1()); + request.setUrl(QUrl(url)); + + QNetworkReply *reply = m_networkManager.get(request); + + QObject::connect + ( + reply, + &QNetworkReply::finished, + this, + [this, reply]() + { + if(reply->error()) + qWarning() << reply->errorString(); + + QByteArray data = reply->readAll(); + QJsonParseError jsonError; + + QJsonDocument document = QJsonDocument::fromJson(data, &jsonError); + + if(jsonError.error != QJsonParseError::NoError) + { + qWarning() << jsonError.errorString(); + return; + } + + QJsonObject rootObject = document.object(); + + if(rootObject.contains(QStringLiteral("prev_page")) && rootObject[QStringLiteral("prev_page")].isString()) + setPreviousPage(rootObject[QStringLiteral("prev_page")].toString()); + else + setPreviousPage(QString()); + + if(rootObject.contains(QStringLiteral("next_page")) && rootObject[QStringLiteral("next_page")].isString()) + setNextPage(rootObject[QStringLiteral("next_page")].toString()); + else + setNextPage(QString()); + + if(rootObject.contains(QStringLiteral("page"))) + setCurrentPage(rootObject[QStringLiteral("page")].toInt()); + else + setCurrentPage(0); + + if(rootObject.contains(QStringLiteral("total_results"))) + setTotalResults(rootObject[QStringLiteral("total_results")].toInt()); + else + setTotalResults(0); + + beginResetModel(); + m_data.clear(); + endResetModel(); + + if(rootObject.contains(QStringLiteral("photos")) && rootObject[QStringLiteral("photos")].isArray()) + { + QJsonArray photoArray = rootObject[QStringLiteral("photos")].toArray(); + + beginInsertRows(QModelIndex(), 0, photoArray.count() - 1); + + for(const QJsonValue &photoRef : std::as_const(photoArray)) + { + if(!photoRef.isObject()) + continue; + + QJsonObject photoObject = photoRef.toObject(); + PexelsImageMetadata photo; + + photo.id = photoObject[QStringLiteral("id")].toInt(); + photo.photographer = photoObject[QStringLiteral("photographer")].toString(); + photo.photographerId = photoObject[QStringLiteral("photographer_id")].toInt(); + photo.photographerUrl = QUrl(photoObject[QStringLiteral("photographer_url")].toString()); + photo.averageColorCode = photoObject[QStringLiteral("avg_color")].toString(); + photo.alt = photoObject[QStringLiteral("alt")].toString(); + photo.width = photoObject[QStringLiteral("width")].toInt(); + photo.height = photoObject[QStringLiteral("height")].toInt(); + photo.url = QUrl(photoObject[QStringLiteral("url")].toString()); + photo.liked = photoObject[QStringLiteral("liked")].toBool(); + + if(photoObject.contains(QStringLiteral("src")) && photoObject[QStringLiteral("src")].isObject()) + { + QJsonObject sourceObject = photoObject[QStringLiteral("src")].toObject(); + QStringList keys = sourceObject.keys(); + + for(const QString &key : std::as_const(keys)) + { + if(key == QStringLiteral("tiny")) + photo.thumbnail = QUrl(sourceObject[key].toString()); + + photo.sources.insert(key, QUrl(sourceObject[key].toString())); + } + } + + m_data.append(photo); + } + + endInsertRows(); + setStatus(Idle); + } + } + ); +} + +int PexelsImageSearchModel::status() const +{ + return static_cast(m_status); +} + +void PexelsImageSearchModel::setStatus(const int &status) +{ + if (m_status == static_cast(status)) + return; + + m_status = static_cast(status); + Q_EMIT statusChanged(); +} + +QString PexelsImageSearchModel::lastSavedFile() const +{ + return m_lastSavedFile; +} + +void PexelsImageSearchModel::setLastSavedFile(const QString &lastSavedFile) +{ + if (m_lastSavedFile == lastSavedFile) + return; + m_lastSavedFile = lastSavedFile; + Q_EMIT lastSavedFileChanged(); +} + +qreal PexelsImageSearchModel::downloadProgress() const +{ + return m_downloadProgress; +} + +void PexelsImageSearchModel::setDownloadProgress(qreal downloadProgress) +{ + if (m_downloadProgress == downloadProgress) + return; + m_downloadProgress = downloadProgress; + Q_EMIT downloadProgressChanged(); +} + +quint64 PexelsImageSearchModel::currentPage() const +{ + return m_currentPage; +} + +void PexelsImageSearchModel::setCurrentPage(quint64 currentPage) +{ + if (m_currentPage == currentPage) + return; + m_currentPage = currentPage; + Q_EMIT currentPageChanged(); +} + +QModelIndex PexelsImageSearchModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return createIndex(row, column, &m_data.at(row)); +} + +int PexelsImageSearchModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 0; +} + +QModelIndex PexelsImageSearchModel::parent(const QModelIndex &index) const +{ + Q_UNUSED(index) + return QModelIndex(); +} + +void PexelsImageSearchModel::next() +{ + if(m_nextPage.isEmpty()) + return; + + getSearchResults(m_nextPage); +} + +void PexelsImageSearchModel::back() +{ + if(m_previousPage.isEmpty()) + return; + + getSearchResults(m_previousPage); +} + +void PexelsImageSearchModel::download(QUrl url, quint64 id) +{ + QNetworkRequest request(url); + QNetworkReply *reply = m_networkManager.get(request); + + QObject::connect + ( + reply, + &QNetworkReply::finished, + this, + [this, reply, id]() + { + if(reply->error()) + qWarning() << reply->errorString(); + + QByteArray data = reply->readAll(); + QPixmap pixmap; + pixmap.loadFromData(data); + + if(pixmap.isNull()) + return; + + QString fileLocation = QStringLiteral("%1/.local/share/komplex/images/%2.png").arg(QStandardPaths::writableLocation(QStandardPaths::HomeLocation), QString::number(id)); + + if(!pixmap.save(fileLocation, "PNG")) + return; + + setLastSavedFile(fileLocation); + + Q_EMIT downloadFinished(); + } + ); + + QObject::connect + ( + reply, + &QNetworkReply::downloadProgress, + this, + [this](qint64 received, qint64 total) + { + setDownloadProgress(static_cast(received) / static_cast(total)); + } + ); +} + +QString PexelsImageSearchModel::previousPage() const +{ + return m_previousPage; +} + +void PexelsImageSearchModel::setPreviousPage(const QString &previousPage) +{ + if (m_previousPage == previousPage) + return; + m_previousPage = previousPage; + Q_EMIT previousPageChanged(); +} + +QString PexelsImageSearchModel::nextPage() const +{ + return m_nextPage; +} + +void PexelsImageSearchModel::setNextPage(const QString &nextPage) +{ + if (m_nextPage == nextPage) + return; + m_nextPage = nextPage; + Q_EMIT nextPageChanged(); +} + +quint64 PexelsImageSearchModel::totalResults() const +{ + return m_totalResults; +} + +void PexelsImageSearchModel::setTotalResults(quint64 totalResults) +{ + if (m_totalResults == totalResults) + return; + m_totalResults = totalResults; + Q_EMIT totalResultsChanged(); +} + +quint16 PexelsImageSearchModel::resultsPerPage() const +{ + return m_resultsPerPage; +} + +void PexelsImageSearchModel::setResultsPerPage(quint16 resultsPerPage) +{ + if (m_resultsPerPage == resultsPerPage) + return; + + m_resultsPerPage = resultsPerPage; + Q_EMIT resultsPerPageChanged(); +} + +QString PexelsImageSearchModel::query() const +{ + return m_query; +} + +void PexelsImageSearchModel::setQuery(const QString &query) +{ + if (m_query == query) + return; + + m_query = query; + Q_EMIT queryChanged(); + + getSearchResults(QStringLiteral("https://api.pexels.com/v1/search?query=%1&per_page=%2").arg(m_query).arg(m_resultsPerPage)); +} diff --git a/plugin/PexelsImageSearch.h b/plugin/PexelsImageSearch.h new file mode 100644 index 0000000..1326264 --- /dev/null +++ b/plugin/PexelsImageSearch.h @@ -0,0 +1,216 @@ +#ifndef PexelsImageSearch_H +#define PexelsImageSearch_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "PexelsImageMetadata.h" +#include "Komplex_global.h" + +class KOMPLEX_EXPORT PexelsImageSearchModel : public QAbstractItemModel +{ + Q_OBJECT +public: + + enum DataRoles + { + Alt = Qt::UserRole + 1, + AverageColor, + Height, + Id, + Liked, + Photographer, + PhotographerId, + PhotographerUrl, + Thumbnail, + Url, + Width, + Original, + Large2x, + Large, + Medium, + Small, + Portrait, + Landscape + }; + Q_ENUM(DataRoles) + + enum Status + { + Idle, + Searching + }; + Q_ENUM(Status) + + PexelsImageSearchModel(QObject *parent = nullptr); + ~PexelsImageSearchModel(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QString query() const; + void setQuery(const QString &query); + + quint16 resultsPerPage() const; + void setResultsPerPage(quint16 resultsPerPage); + + quint64 totalResults() const; + void setTotalResults(quint64 totalResults); + + QString nextPage() const; + void setNextPage(const QString &nextPage); + + QString previousPage() const; + void setPreviousPage(const QString &previousPage); + + quint64 currentPage() const; + void setCurrentPage(quint64 currentPage); + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + + Q_INVOKABLE void next(); + Q_INVOKABLE void back(); + Q_INVOKABLE void download(QUrl url, quint64 id); + + qreal downloadProgress() const; + void setDownloadProgress(qreal downloadProgress); + + QString lastSavedFile() const; + void setLastSavedFile(const QString &lastSavedFile); + + int status() const; + void setStatus(const int &status); + +Q_SIGNALS: + void queryChanged(); + void resultsPerPageChanged(); + void totalResultsChanged(); + void nextPageChanged(); + void previousPageChanged(); + void currentPageChanged(); + void downloadProgressChanged(); + void downloadFinished(); + void lastSavedFileChanged(); + void statusChanged(); + +protected: + QHash roleNames() const override; + +private: + void getSearchResults(QString url); + + QNetworkAccessManager m_networkManager; + QString m_query; + + quint16 m_resultsPerPage = 9; + quint64 m_totalResults = 0; + quint64 m_currentPage = 0; + qreal m_downloadProgress = 0; + QString m_nextPage; + QString m_previousPage; + QString m_lastSavedFile; + + QList m_data; + Status m_status = Status::Idle; + + static inline const QHash m_dataRoles = + { + { + static_cast(Alt), + QByteArray("alt") + }, + { + static_cast(AverageColor), + QByteArray("averageColor") + }, + { + static_cast(Height), + QByteArray("imageHeight") + }, + { + static_cast(Id), + QByteArray("id") + }, + { + static_cast(Liked), + QByteArray("liked") + }, + { + static_cast(Photographer), + QByteArray("photographer") + }, + { + static_cast(PhotographerId), + QByteArray("photographerId") + }, + { + static_cast(PhotographerUrl), + QByteArray("photographerUrl") + }, + { + static_cast(Thumbnail), + QByteArray("thumbnail") + }, + { + static_cast(Url), + QByteArray("url") + }, + { + static_cast(Width), + QByteArray("imageWidth") + }, + { + static_cast(Original), + QByteArray("original") + }, + { + static_cast(Large2x), + QByteArray("large2x") + }, + { + static_cast(Large), + QByteArray("large") + }, + { + static_cast(Medium), + QByteArray("medium") + }, + { + static_cast(Small), + QByteArray("small") + }, + { + static_cast(Portrait), + QByteArray("portrait") + }, + { + static_cast(Landscape), + QByteArray("landscape") + } + }; + + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged FINAL) + Q_PROPERTY(quint16 resultsPerPage READ resultsPerPage WRITE setResultsPerPage NOTIFY resultsPerPageChanged FINAL) + Q_PROPERTY(quint64 totalResults READ totalResults WRITE setTotalResults NOTIFY totalResultsChanged FINAL) + Q_PROPERTY(QString nextPage READ nextPage WRITE setNextPage NOTIFY nextPageChanged FINAL) + Q_PROPERTY(QString previousPage READ previousPage WRITE setPreviousPage NOTIFY previousPageChanged FINAL) + Q_PROPERTY(quint64 currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged FINAL) + Q_PROPERTY(qreal downloadProgress READ downloadProgress WRITE setDownloadProgress NOTIFY downloadProgressChanged FINAL) + Q_PROPERTY(QString lastSavedFile READ lastSavedFile WRITE setLastSavedFile NOTIFY lastSavedFileChanged FINAL) + Q_PROPERTY(int status READ status WRITE setStatus NOTIFY statusChanged FINAL) +}; +Q_DECLARE_METATYPE(PexelsImageSearchModel) +#endif // PexelsImageSearch_H diff --git a/plugin/PexelsVideoMetadata.h b/plugin/PexelsVideoMetadata.h new file mode 100644 index 0000000..eb34a72 --- /dev/null +++ b/plugin/PexelsVideoMetadata.h @@ -0,0 +1,63 @@ +#ifndef Metadata_H +#define Metadata_H +#include + +#include "Komplex_global.h" + +struct KOMPLEX_EXPORT PexelsVideoThumbnail +{ + quint64 id = 0; + quint64 nr = 0; + + QString image; +}; + +struct KOMPLEX_EXPORT PexelsVideoMetadata +{ + quint64 id = 0; + quint64 width = 0; + quint64 height = 0; + quint64 size = 0; + qreal fps; + QString quality; + QString type; + QString link; + QString sizeText; + + // bool operator==(const PexelsVideoMetadata &other) const + // { + // return ((id == other.id) && (height == other.height) && + // (fps == other.fps) && (link == other.link) && + // (quality == other.quality) && (type == other.type) && + // (width == other.width) && (size == other.size) && + // (sizeText == other.sizeText)); + // } + + // bool operator!=(const PexelsVideoMetadata &other) const + // { + // return !(*this == other); + // } +}; + +struct KOMPLEX_EXPORT PexelsVideoUser +{ + quint64 id; + QString name; + QString url; +}; + +struct KOMPLEX_EXPORT PexelsVideoEntry +{ + quint64 id = 0; + quint64 width = 0; + quint64 height = 0; + quint64 duration = 0; + QString url; + QString image; + QStringList tags; + PexelsVideoUser author; + QList videos; + QList thumbnails; +}; + +#endif // Metadata_H diff --git a/plugin/PexelsVideoModel.cpp b/plugin/PexelsVideoModel.cpp new file mode 100644 index 0000000..0a270b9 --- /dev/null +++ b/plugin/PexelsVideoModel.cpp @@ -0,0 +1,293 @@ +#include "PexelsVideoModel.h" +#include + +PexelsVideoEntryModel::PexelsVideoEntryModel(QObject *parent) + : QAbstractItemModel{parent} +{ + m_networkManager.setAutoDeleteReplies(true); +} + +int PexelsVideoEntryModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_data.size(); +} + +QVariant PexelsVideoEntryModel::data(const QModelIndex &index, int role) const +{ + if(index.row() < 0 || index.row() >= m_data.count()) + return QVariant(); + + QVariant data; + + switch (static_cast(role)) + { + case Fps: + data = QVariant::fromValue(m_data[index.row()].fps); + break; + case Height: + data = QVariant::fromValue(m_data[index.row()].height); + break; + case Id: + data = QVariant::fromValue(m_data[index.row()].id); + break; + case Url: + data = QVariant::fromValue(m_data[index.row()].link); + break; + case Width: + data = QVariant::fromValue(m_data[index.row()].width); + break; + case Type: + data = QVariant::fromValue(m_data[index.row()].type); + break; + case Quality: + data = QVariant::fromValue(m_data[index.row()].quality); + break; + case Text: + data = QVariant::fromValue(QStringLiteral("%1 %2x%3 (%4)").arg(m_data[index.row()].quality.toUpper(),QString::number(m_data[index.row()].width),QString::number(m_data[index.row()].height),m_data[index.row()].sizeText)); + break; + case Size: + data = QVariant::fromValue(m_data[index.row()].size); + break; + } + + return data; +} + +QHash PexelsVideoEntryModel::roleNames() const +{ + return m_dataRoles; +} + +PexelsVideoEntryModel::Status PexelsVideoEntryModel::status() const +{ + return m_status; +} + +void PexelsVideoEntryModel::setStatus(const Status &status) +{ + if (m_status == status) + return; + m_status = status; + Q_EMIT statusChanged(); +} + +QModelIndex PexelsVideoEntryModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return createIndex(row, column, &m_data.at(row)); +} + +int PexelsVideoEntryModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 0; +} + +QModelIndex PexelsVideoEntryModel::parent(const QModelIndex &index) const +{ + Q_UNUSED(index) + return QModelIndex(); +} + +void PexelsVideoEntryModel::setMetadata(const QList &data) +{ + beginResetModel(); + m_data.clear(); + endResetModel(); + + beginInsertRows(QModelIndex(), 0, data.count() - 1); + m_data = data; + endInsertRows(); +} + +bool PexelsVideoEntryModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if(index.row() < 0 || index.row() >= m_data.count()) + return false; + + PexelsVideoMetadata entry = m_data[index.row()]; + + switch(static_cast(role)) + { + case Fps: + entry.fps = value.toDouble(); + break; + case Height: + entry.height = value.toInt(); + break; + case Id: + entry.id = value.toInt(); + break; + case Url: + entry.link = value.toString(); + break; + case Width: + entry.width = value.toInt(); + break; + case Type: + entry.type = value.toString(); + break; + case Quality: + entry.quality = value.toString(); + break; + case Text: + break; + case Size: + entry.size = value.toInt(); + break; + } + + beginInsertRows(index, index.row(), index.row()); + m_data.replace(index.row(), entry); + endInsertRows(); + + return true; +} + +void PexelsVideoEntryModel::download(quint64 index) +{ + QNetworkRequest request(QUrl(m_data[index].link)); + QNetworkReply *reply = m_networkManager.get(request); + + QObject::connect + ( + reply, + &QNetworkReply::finished, + this, + [this, reply, index]() + { + if(reply->error()) + qWarning() << reply->errorString(); + + QByteArray data = reply->readAll(); + QString fileLocation = QStringLiteral("%1/.local/share/komplex/videos/%2.%3").arg(QStandardPaths::writableLocation(QStandardPaths::HomeLocation), QString::number(m_data[index].id), m_data[index].type.mid(m_data[index].type.lastIndexOf(QLatin1Char('/')) + 1)); + QFile file(fileLocation); + + if(!file.open(QFile::WriteOnly)) + { + qWarning() << QStringLiteral("Could not download file"); + return; + } + + qint64 bytesWritten = file.write(data); + + if(static_cast(bytesWritten) != data.length()) + qWarning() << QStringLiteral("Could not save file. %1 of %2").arg(bytesWritten).arg(data.length()); + + file.close(); + + setLastSavedFile(fileLocation); + + Q_EMIT downloadFinished(); + } + ); + + QObject::connect + ( + reply, + &QNetworkReply::downloadProgress, + this, + [this](qint64 received, qint64 total) + { + setDownloadProgress(static_cast(received) / static_cast(total)); + } + ); +} + +void PexelsVideoEntryModel::update() +{ + setStatus(Loading); + + for(int i = 0; i < m_data.count(); ++i) + { + m_data[i].size = getFileSize(QUrl(m_data[i].link)); + m_data[i].sizeText = sizeText(m_data[i].size); + QThread::msleep(100); + } + + setStatus(Idle); +} + +qreal PexelsVideoEntryModel::downloadProgress() const +{ + return m_downloadProgress; +} + +void PexelsVideoEntryModel::setDownloadProgress(qreal downloadProgress) +{ + if (qFuzzyCompare(m_downloadProgress, downloadProgress)) + return; + m_downloadProgress = downloadProgress; + Q_EMIT downloadProgressChanged(); +} + +QString PexelsVideoEntryModel::lastSavedFile() const +{ + return m_lastSavedFile; +} + +void PexelsVideoEntryModel::setLastSavedFile(const QString &lastSavedFile) +{ + if (m_lastSavedFile == lastSavedFile) + return; + + m_lastSavedFile = lastSavedFile; + Q_EMIT lastSavedFileChanged(); +} + +quint64 PexelsVideoEntryModel::getFileSize(QUrl url) +{ + quint64 size = 0; + + QEventLoop loop; + + QNetworkRequest request; + request.setUrl(url); + + QNetworkReply *reply = m_networkManager.head(request); + QObject::connect( + reply, + &QNetworkReply::finished, + this, + [reply, &loop, &size]() + { + if(reply->error()) + { + qWarning() << QStringLiteral("Failed to download header for file size"); + return; + } + + if(reply->hasRawHeader(QStringLiteral("Content-Length"))) + { + QByteArray headerData = reply->rawHeader(QStringLiteral("Content-Length")); + + if(!headerData.isValidUtf8()) + { + qWarning() << QStringLiteral("Invalid header data format"); + return; + } + + QString data = QString::fromUtf8(headerData); + size = data.toInt(); + } + + loop.quit(); + } + ); + + if(!reply->isFinished()) + loop.exec(); + + return size; +} + +QString PexelsVideoEntryModel::sizeText(quint64 size) +{ + int index = 0; + + for(;index < m_sizeSuffix.count() && size >= 1000; index++) + size /= 1000; + + return QStringLiteral("%1%2").arg(QString::number(size), m_sizeSuffix[index]); +} diff --git a/plugin/PexelsVideoModel.h b/plugin/PexelsVideoModel.h new file mode 100644 index 0000000..bc66010 --- /dev/null +++ b/plugin/PexelsVideoModel.h @@ -0,0 +1,149 @@ +#ifndef PexelsVideoEntryModel_H +#define PexelsVideoEntryModel_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "PexelsVideoMetadata.h" +#include "Komplex_global.h" + + +class KOMPLEX_EXPORT PexelsVideoEntryModel : public QAbstractItemModel +{ + Q_OBJECT +public: + + enum DataRoles + { + Type = Qt::UserRole + 1, + Height, + Id, + Quality, + Url, + Width, + Fps, + Size, + Text + }; + Q_ENUM(DataRoles) + + enum Status + { + Idle, + Loading + }; + Q_ENUM(Status) + + PexelsVideoEntryModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + + virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + void setMetadata(const QList &data); + + QString lastSavedFile() const; + void setLastSavedFile(const QString &lastSavedFile); + + qreal downloadProgress() const; + void setDownloadProgress(qreal downloadProgress); + Q_INVOKABLE void download(quint64 index); + Q_INVOKABLE void update(); + + Status status() const; + void setStatus(const Status &status); + +Q_SIGNALS: + void downloadProgressChanged(); + void downloadFinished(); + void lastSavedFileChanged(); + void statusChanged(); + +protected: + QHash roleNames() const override; + +private: + quint64 getFileSize(QUrl url); + QString sizeText(quint64 size); + + QNetworkAccessManager m_networkManager; + QString m_query; + + QList m_data; + QString m_lastSavedFile; + qreal m_downloadProgress = 0; + Status m_status = Status::Idle; + + static inline const QHash m_dataRoles = + { + { + static_cast(Height), + QByteArray("videoHeight") + }, + { + static_cast(Id), + QByteArray("id") + }, + { + static_cast(Quality), + QByteArray("quality") + }, + { + static_cast(Url), + QByteArray("url") + }, + { + static_cast(Width), + QByteArray("videoWidth") + }, + { + static_cast(Fps), + QByteArray("fps") + }, + { + static_cast(Type), + QByteArray("videoType") + }, + { + static_cast(Text), + QByteArray("text") + }, + { + static_cast(Size), + QByteArray("size") + } + }; + + static inline const QStringList m_sizeSuffix = + { + QStringLiteral("B"), + QStringLiteral("KB"), + QStringLiteral("MB"), + QStringLiteral("GB"), + QStringLiteral("TB") + }; + + Q_PROPERTY(QString lastSavedFile READ lastSavedFile WRITE setLastSavedFile NOTIFY lastSavedFileChanged FINAL) + Q_PROPERTY(qreal downloadProgress READ downloadProgress WRITE setDownloadProgress NOTIFY downloadProgressChanged FINAL) + Q_PROPERTY(Status status READ status WRITE setStatus NOTIFY statusChanged FINAL) +}; + +Q_DECLARE_METATYPE(PexelsVideoEntryModel) + +#endif // PexelsVideoEntryModel_H diff --git a/plugin/PexelsVideoSearch.cpp b/plugin/PexelsVideoSearch.cpp new file mode 100644 index 0000000..941db20 --- /dev/null +++ b/plugin/PexelsVideoSearch.cpp @@ -0,0 +1,383 @@ +#include "PexelsVideoSearch.h" +#include "PexelsAPI.h" +#include + +PexelsVideoSearchModel::PexelsVideoSearchModel(QObject *parent) : QAbstractItemModel { parent } +{ + // m_cache.setMaxCost(1024); + m_videoModel = new PexelsVideoEntryModel(this); + + QObject::connect(m_videoModel, &PexelsVideoEntryModel::lastSavedFileChanged, this, &PexelsVideoSearchModel::lastSavedFileChanged); +} + +PexelsVideoSearchModel::~PexelsVideoSearchModel() +{ + if(m_videoModel) + m_videoModel->deleteLater(); +} + +int PexelsVideoSearchModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_data.size(); +} + +QVariant PexelsVideoSearchModel::data(const QModelIndex &index, int role) const +{ + if(index.row() < 0 || index.row() >= m_data.count()) + return QVariant(); + + QVariant data; + + switch (static_cast(role)) { + case Tags: + data = QVariant::fromValue(m_data[index.row()].tags); + break; + case Height: + data = QVariant::fromValue(m_data[index.row()].height); + break; + case Id: + data = QVariant::fromValue(m_data[index.row()].id); + break; + case User: + data = QVariant::fromValue(m_data[index.row()].author.name); + break; + case UserId: + data = QVariant::fromValue(m_data[index.row()].author.id); + break; + case UserUrl: + data = QVariant::fromValue(m_data[index.row()].author.url); + break; + case Thumbnail: + if(m_data[index.row()].thumbnails.count() > 0) + data = QVariant::fromValue(m_data[index.row()].thumbnails[0].image); + break; + case Url: + data = QVariant::fromValue(m_data[index.row()].url); + break; + case Width: + data = QVariant::fromValue(m_data[index.row()].width); + break; + } + + return data; +} + +QHash PexelsVideoSearchModel::roleNames() const +{ + return m_dataRoles; +} + +void PexelsVideoSearchModel::getSearchResults(QString url) +{ + setStatus(Searching); + + QNetworkRequest request; + request.setRawHeader(QStringLiteral("Authorization").toLatin1(), QStringLiteral(PAK).toLatin1()); + request.setUrl(QUrl(url)); + + QNetworkReply *reply = m_networkManager.get(request); + + QObject::connect + ( + reply, + &QNetworkReply::finished, + this, + [this, reply]() + { + if(reply->error()) + qWarning() << reply->errorString(); + + QByteArray data = reply->readAll(); + QJsonParseError jsonError; + + QJsonDocument document = QJsonDocument::fromJson(data, &jsonError); + + if(jsonError.error != QJsonParseError::NoError) + { + qWarning() << jsonError.errorString(); + return; + } + + QJsonObject rootObject = document.object(); + + if(rootObject.contains(QStringLiteral("prev_page")) && rootObject[QStringLiteral("prev_page")].isString()) + setPreviousPage(rootObject[QStringLiteral("prev_page")].toString()); + else + setPreviousPage(QString()); + + if(rootObject.contains(QStringLiteral("next_page")) && rootObject[QStringLiteral("next_page")].isString()) + setNextPage(rootObject[QStringLiteral("next_page")].toString()); + else + setNextPage(QString()); + + if(rootObject.contains(QStringLiteral("page"))) + setCurrentPage(rootObject[QStringLiteral("page")].toInt()); + else + setCurrentPage(0); + + if(rootObject.contains(QStringLiteral("total_results"))) + setTotalResults(rootObject[QStringLiteral("total_results")].toInt()); + else + setTotalResults(0); + + beginResetModel(); + m_data.clear(); + endResetModel(); + + if(rootObject.contains(QStringLiteral("videos")) && rootObject[QStringLiteral("videos")].isArray()) + { + QJsonArray videoArray = rootObject[QStringLiteral("videos")].toArray(); + + beginInsertRows(QModelIndex(), 0, videoArray.count() - 1); + + for(const QJsonValue &videoRef : std::as_const(videoArray)) + { + if(!videoRef.isObject()) + continue; + + QJsonObject videoObject = videoRef.toObject(); + PexelsVideoEntry video; + + video.id = videoObject[QStringLiteral("id")].toInt(); + video.url = videoObject[QStringLiteral("url")].toString(); + video.image = videoObject[QStringLiteral("image")].toString(); + video.width = videoObject[QStringLiteral("width")].toInt(); + video.height = videoObject[QStringLiteral("height")].toInt(); + video.url = videoObject[QStringLiteral("url")].toString(); + video.duration = videoObject[QStringLiteral("tags")].toInt(); + + if(videoObject.contains(QStringLiteral("tags")) && videoObject[QStringLiteral("tags")].isArray()) + { + QJsonArray tagsArray = videoObject[QStringLiteral("tags")].toArray(); + + for(const QJsonValue &tagRef : std::as_const(tagsArray)) + video.tags.append(tagRef.toString()); + } + + if(videoObject.contains(QStringLiteral("user")) && videoObject[QStringLiteral("user")].isObject()) + { + QJsonObject userObject = videoObject[QStringLiteral("user")].toObject(); + video.author.name = userObject[QStringLiteral("name")].toString(); + video.author.id = userObject[QStringLiteral("id")].toInt(); + video.author.url = userObject[QStringLiteral("url")].toString(); + } + + if(videoObject.contains(QStringLiteral("video_files")) && videoObject[QStringLiteral("video_files")].isArray()) + { + QJsonArray sourceObject = videoObject[QStringLiteral("video_files")].toArray(); + + for(const QJsonValue &sourceObject : std::as_const(sourceObject)) + { + QJsonObject metaObject = sourceObject.toObject(); + + PexelsVideoMetadata metadata; + metadata.fps = metaObject[QStringLiteral("fps")].toDouble(); + metadata.height = metaObject[QStringLiteral("height")].toInt(); + metadata.id = metaObject[QStringLiteral("id")].toInt(); + metadata.link = metaObject[QStringLiteral("link")].toString(); + metadata.quality = metaObject[QStringLiteral("quality")].toString(); + metadata.type = metaObject[QStringLiteral("file_type")].toString(); + metadata.width = metaObject[QStringLiteral("width")].toInt(); + + video.videos.append(metadata); + } + } + + if(videoObject.contains(QStringLiteral("video_pictures")) && videoObject[QStringLiteral("video_pictures")].isArray()) + { + QJsonArray sourceObject = videoObject[QStringLiteral("video_pictures")].toArray(); + + for(const QJsonValue &sourceObject : std::as_const(sourceObject)) + { + QJsonObject metaObject = sourceObject.toObject(); + + struct PexelsVideoThumbnail metadata; + metadata.image = metaObject[QStringLiteral("picture")].toString(); + metadata.nr = metaObject[QStringLiteral("nr")].toInt(); + metadata.id = metaObject[QStringLiteral("id")].toInt(); + video.thumbnails.append(metadata); + } + } + + m_data.append(video); + } + + endInsertRows(); + setCurrentIndex(0); + setStatus(Idle); + } + } + ); +} + +QString PexelsVideoSearchModel::sizeText(quint64 size) +{ + int index = 0; + + for(;index < m_sizeSuffix.count() && size >= 1000; index++) + size /= 1000; + + return QStringLiteral("%1%2").arg(QString::number(size), m_sizeSuffix[index]); +} + +quint64 PexelsVideoSearchModel::currentIndex() const +{ + return m_currentIndex; +} + +void PexelsVideoSearchModel::setCurrentIndex(quint64 currentIndex) +{ + if (currentIndex < 0 || static_cast(currentIndex) >= m_data.count()) + { + m_videoModel->setMetadata(QList()); + return; + } + + m_videoModel->setMetadata(m_data[currentIndex].videos); + + m_currentIndex = currentIndex; + Q_EMIT currentIndexChanged(); + Q_EMIT videoModelChanged(); +} + +PexelsVideoEntryModel *PexelsVideoSearchModel::videoModel() const +{ + return m_videoModel; +} + +int PexelsVideoSearchModel::status() const +{ + return static_cast(m_status); +} + +void PexelsVideoSearchModel::setStatus(const int &status) +{ + if (m_status == static_cast(status)) + return; + + m_status = static_cast(status); + Q_EMIT statusChanged(); +} + +QString PexelsVideoSearchModel::lastSavedFile() const +{ + return m_videoModel->lastSavedFile(); +} + +quint64 PexelsVideoSearchModel::currentPage() const +{ + return m_currentPage; +} + +void PexelsVideoSearchModel::setCurrentPage(quint64 currentPage) +{ + if (m_currentPage == currentPage) + return; + m_currentPage = currentPage; + Q_EMIT currentPageChanged(); +} + +QModelIndex PexelsVideoSearchModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return createIndex(row, column, &m_data.at(row)); +} + +int PexelsVideoSearchModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 0; +} + +QModelIndex PexelsVideoSearchModel::parent(const QModelIndex &index) const +{ + Q_UNUSED(index) + return QModelIndex(); +} + +void PexelsVideoSearchModel::next() +{ + if(m_nextPage.isEmpty()) + return; + + getSearchResults(m_nextPage); +} + +void PexelsVideoSearchModel::back() +{ + if(m_previousPage.isEmpty()) + return; + + getSearchResults(m_previousPage); +} + +QString PexelsVideoSearchModel::previousPage() const +{ + return m_previousPage; +} + +void PexelsVideoSearchModel::setPreviousPage(const QString &previousPage) +{ + if (m_previousPage == previousPage) + return; + m_previousPage = previousPage; + Q_EMIT previousPageChanged(); +} + +QString PexelsVideoSearchModel::nextPage() const +{ + return m_nextPage; +} + +void PexelsVideoSearchModel::setNextPage(const QString &nextPage) +{ + if (m_nextPage == nextPage) + return; + + m_nextPage = nextPage; + Q_EMIT nextPageChanged(); +} + +quint64 PexelsVideoSearchModel::totalResults() const +{ + return m_totalResults; +} + +void PexelsVideoSearchModel::setTotalResults(quint64 totalResults) +{ + if (m_totalResults == totalResults) + return; + m_totalResults = totalResults; + Q_EMIT totalResultsChanged(); +} + +quint16 PexelsVideoSearchModel::resultsPerPage() const +{ + return m_resultsPerPage; +} + +void PexelsVideoSearchModel::setResultsPerPage(quint16 resultsPerPage) +{ + if (m_resultsPerPage == resultsPerPage) + return; + + m_resultsPerPage = resultsPerPage; + Q_EMIT resultsPerPageChanged(); +} + +QString PexelsVideoSearchModel::query() const +{ + return m_query; +} + +void PexelsVideoSearchModel::setQuery(const QString &query) +{ + if (m_query == query) + return; + + m_query = query; + Q_EMIT queryChanged(); + + getSearchResults(QStringLiteral("https://api.pexels.com/videos/search?query=%1&per_page=%2").arg(QUrl::toPercentEncoding(m_query)).arg(m_resultsPerPage)); +} diff --git a/plugin/PexelsVideoSearch.h b/plugin/PexelsVideoSearch.h new file mode 100644 index 0000000..49ba20b --- /dev/null +++ b/plugin/PexelsVideoSearch.h @@ -0,0 +1,196 @@ +#ifndef PEXELS_VIDEO_SEARCH_MODEL_H +#define PEXELS_VIDEO_SEARCH_MODEL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "PexelsVideoModel.h" +#include "PexelsVideoMetadata.h" +#include "Komplex_global.h" + +class KOMPLEX_EXPORT PexelsVideoSearchModel : public QAbstractItemModel +{ + Q_OBJECT +public: + + enum DataRoles + { + Tags = Qt::UserRole + 1, + Height, + Id, + User, + UserId, + UserUrl, + Thumbnail, + Url, + Width + }; + Q_ENUM(DataRoles) + + enum Status + { + Idle, + Searching + }; + Q_ENUM(Status) + + PexelsVideoSearchModel(QObject *parent = nullptr); + ~PexelsVideoSearchModel(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QString query() const; + void setQuery(const QString &query); + + quint16 resultsPerPage() const; + void setResultsPerPage(quint16 resultsPerPage); + + quint64 totalResults() const; + void setTotalResults(quint64 totalResults); + + QString nextPage() const; + void setNextPage(const QString &nextPage); + + QString previousPage() const; + void setPreviousPage(const QString &previousPage); + + quint64 currentPage() const; + void setCurrentPage(quint64 currentPage); + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + + Q_INVOKABLE void next(); + Q_INVOKABLE void back(); + + qreal downloadProgress() const; + void setDownloadProgress(qreal downloadProgress); + + QString lastSavedFile() const; + void setLastSavedFile(const QString &lastSavedFile); + + int status() const; + void setStatus(const int &status); + + PexelsVideoEntryModel *videoModel() const; + + quint64 currentIndex() const; + void setCurrentIndex(quint64 currentIndex); + +Q_SIGNALS: + void queryChanged(); + void resultsPerPageChanged(); + void totalResultsChanged(); + void nextPageChanged(); + void previousPageChanged(); + void currentPageChanged(); + void downloadProgressChanged(); + void downloadFinished(); + void lastSavedFileChanged(); + void statusChanged(); + void videoModelChanged(); + + void currentIndexChanged(); + +protected: + QHash roleNames() const override; + +private: + void getSearchResults(QString url); + quint64 getFileSize(QUrl url); + QString sizeText(quint64 size); + + QNetworkAccessManager m_networkManager; + QString m_query; + + quint16 m_resultsPerPage = 9; + quint64 m_totalResults = 0; + quint64 m_currentPage = 0; + quint64 m_currentIndex = 0; + qreal m_downloadProgress = 0; + QString m_nextPage; + QString m_previousPage; + QString m_lastSavedFile; + + PexelsVideoEntryModel *m_videoModel = nullptr; + + QList m_data; + Status m_status = Status::Idle; + + static inline const QHash m_dataRoles = + { + { + static_cast(Tags), + QByteArray("tags") + }, + { + static_cast(Height), + QByteArray("videoHeight") + }, + { + static_cast(Id), + QByteArray("id") + }, + { + static_cast(User), + QByteArray("user") + }, + { + static_cast(UserId), + QByteArray("userId") + }, + { + static_cast(UserUrl), + QByteArray("userUrl") + }, + { + static_cast(Thumbnail), + QByteArray("thumbnail") + }, + { + static_cast(Url), + QByteArray("videoUrl") + }, + { + static_cast(Width), + QByteArray("videoWidth") + } + }; + + static inline const QStringList m_sizeSuffix = + { + QStringLiteral("B"), + QStringLiteral("KB"), + QStringLiteral("MB"), + QStringLiteral("GB"), + QStringLiteral("TB") + }; + + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged FINAL) + Q_PROPERTY(quint16 resultsPerPage READ resultsPerPage WRITE setResultsPerPage NOTIFY resultsPerPageChanged FINAL) + Q_PROPERTY(quint64 totalResults READ totalResults WRITE setTotalResults NOTIFY totalResultsChanged FINAL) + Q_PROPERTY(QString nextPage READ nextPage WRITE setNextPage NOTIFY nextPageChanged FINAL) + Q_PROPERTY(QString previousPage READ previousPage WRITE setPreviousPage NOTIFY previousPageChanged FINAL) + Q_PROPERTY(quint64 currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged FINAL) + Q_PROPERTY(QString lastSavedFile READ lastSavedFile NOTIFY lastSavedFileChanged FINAL) + Q_PROPERTY(int status READ status WRITE setStatus NOTIFY statusChanged FINAL) + Q_PROPERTY(PexelsVideoEntryModel *videoModel READ videoModel NOTIFY videoModelChanged FINAL) + Q_PROPERTY(quint64 currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged FINAL) +}; + +Q_DECLARE_METATYPE(PexelsVideoSearchModel) + +#endif // PexelsVideoSearchModel_H