diff --git a/docs/_posts/2023-07-23-grabber-7-11-2-released.md b/docs/_posts/2023-07-23-grabber-7-11-2-released.md new file mode 100644 index 000000000..b074e28d4 --- /dev/null +++ b/docs/_posts/2023-07-23-grabber-7-11-2-released.md @@ -0,0 +1,11 @@ +--- +title: "Grabber 7.11.2 released" +date: 2023-07-23 21:45 +0200 +categories: release +--- + + +Grabber 7.11.2 has been released. + +The list of changes and download links can be found on Github: + \ No newline at end of file diff --git a/scripts/windows-setup/setup.iss b/scripts/windows-setup/setup.iss index ba72ce5e1..fdbe97fd6 100644 --- a/scripts/windows-setup/setup.iss +++ b/scripts/windows-setup/setup.iss @@ -22,7 +22,7 @@ #endif #ifndef MyAppVersion -# define MyAppVersion "7.11.1" +# define MyAppVersion "7.11.2" #endif #ifndef QtApngDll diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 751f3f4de..76fdfb5be 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,7 +37,7 @@ if(TRACE) endif() if((NOT DEFINED VERSION) OR ((DEFINED NIGHTLY) AND (NIGHTLY MATCHES "1"))) - set(VERSION "7.11.1") + set(VERSION "7.11.2") else() string(REGEX REPLACE "^v" "" VERSION "${VERSION}") endif() diff --git a/src/dist/linux/org.bionus.Grabber.metainfo.xml b/src/dist/linux/org.bionus.Grabber.metainfo.xml index 9a3841425..cfa946d66 100644 --- a/src/dist/linux/org.bionus.Grabber.metainfo.xml +++ b/src/dist/linux/org.bionus.Grabber.metainfo.xml @@ -43,6 +43,7 @@ https://github.com/Bionus/imgbrd-grabber/blob/master/CONTRIBUTING.md + diff --git a/src/gui/src/viewer/viewer-window.cpp b/src/gui/src/viewer/viewer-window.cpp index 354323f16..6f90fb25c 100644 --- a/src/gui/src/viewer/viewer-window.cpp +++ b/src/gui/src/viewer/viewer-window.cpp @@ -175,9 +175,8 @@ ViewerWindow::ViewerWindow(QList> images, const QSharedPoi setStyleSheet("#ViewerWindow { background-color:" + bg + "; }"); } - m_resizeTimer = new QTimer(this); - connect(m_resizeTimer, SIGNAL(timeout()), this, SLOT(update())); - m_resizeTimer->setSingleShot(true); + connect(&m_resizeTimer, SIGNAL(timeout()), this, SLOT(update())); + m_resizeTimer.setSingleShot(true); load(image); } @@ -1201,11 +1200,11 @@ void ViewerWindow::toggleSlideshow() void ViewerWindow::resizeEvent(QResizeEvent *e) { - if (!m_resizeTimer->isActive()) { + if (!m_resizeTimer.isActive()) { m_timeout = qMin(500, qMax(50, (m_displayImage.width() * m_displayImage.height()) / 100000)); } - m_resizeTimer->stop(); - m_resizeTimer->start(m_timeout); + m_resizeTimer.stop(); + m_resizeTimer.start(m_timeout); update(true); QWidget::resizeEvent(e); diff --git a/src/gui/src/viewer/viewer-window.h b/src/gui/src/viewer/viewer-window.h index 1ab1be98e..e82c14c4a 100644 --- a/src/gui/src/viewer/viewer-window.h +++ b/src/gui/src/viewer/viewer-window.h @@ -139,7 +139,7 @@ class ViewerWindow : public QWidget bool m_pendingClose; bool m_tooBig, m_loadedImage, m_loadedDetails; QAffiche *m_labelTagsTop, *m_labelTagsLeft; - QTimer *m_resizeTimer; + QTimer m_resizeTimer; QElapsedTimer m_imageTime; QString m_link; bool m_finished; diff --git a/src/lib/src/analytics.cpp b/src/lib/src/analytics.cpp index a43685872..d417e78e6 100644 --- a/src/lib/src/analytics.cpp +++ b/src/lib/src/analytics.cpp @@ -1,4 +1,5 @@ #include "analytics.h" +#include class QString; class QVariant; @@ -62,6 +63,8 @@ void Analytics::sendScreenView(const QString& screenName, const QVariantMap& cus QVariantMap eventParams(customValues); eventParams["firebase_screen"] = screenName; eventParams["firebase_screen_class"] = screenName; + eventParams["app_name"] = qApp->applicationName(); + eventParams["app_version"] = qApp->applicationVersion(); m_ga4.sendEvent("screen_view", eventParams); } diff --git a/src/lib/src/downloader/image-downloader.cpp b/src/lib/src/downloader/image-downloader.cpp index f4fbfc364..e84008b07 100644 --- a/src/lib/src/downloader/image-downloader.cpp +++ b/src/lib/src/downloader/image-downloader.cpp @@ -60,7 +60,7 @@ void ImageDownloader::setSize(Image::Size size) { if (size == Image::Size::Unknown) { const bool getOriginals = m_profile->getSettings()->value("Save/downloadoriginals", true).toBool(); - const bool hasSample = m_image->url(Image::Size::Sample).isEmpty(); + const bool hasSample = !m_image->url(Image::Size::Sample).isEmpty(); if (getOriginals || !hasSample) { m_size = Image::Size::Full; } else { diff --git a/src/lib/src/login/oauth2-login.cpp b/src/lib/src/login/oauth2-login.cpp index 08ae22751..9ae67d46c 100644 --- a/src/lib/src/login/oauth2-login.cpp +++ b/src/lib/src/login/oauth2-login.cpp @@ -27,6 +27,7 @@ OAuth2Login::OAuth2Login(OAuth2Auth *auth, Site *site, NetworkManager *manager, { m_accessToken = m_settings->value("auth/accessToken").toString(); m_refreshToken = m_settings->value("auth/refreshToken").toString(); + m_expires = m_settings->value("auth/accessTokenExpiration").toDateTime(); } bool OAuth2Login::isTestable() const @@ -200,8 +201,10 @@ void OAuth2Login::loginAuthorizationCode() m_accessToken = flow->token(); m_refreshToken = flow->refreshToken(); + m_expires = flow->expirationAt(); m_settings->setValue("auth/accessToken", m_accessToken); m_settings->setValue("auth/refreshToken", m_refreshToken); + m_settings->setValue("auth/accessTokenExpiration", m_expires); emit loggedIn(Result::Success); @@ -300,19 +303,30 @@ void OAuth2Login::basicRefresh() void OAuth2Login::refresh(bool login) { - log(QStringLiteral("[%1] Refreshing OAuth2 token...").arg(m_site->url()), Logger::Info); + // Don't try to refresh while a refresh is already in progress + if (m_refreshing) { + if (login) { + m_refreshForLogin = true; + } + return; + } - const QString consumerKey = m_settings->value("auth/consumerKey").toString(); - const QString consumerSecret = m_settings->value("auth/consumerSecret").toString(); + log(QStringLiteral("[%1] Refreshing OAuth2 token...").arg(m_site->url()), Logger::Info); + // Without a refresh token, there's nothing to do if (m_refreshToken.isEmpty()) { log(QStringLiteral("[%1] Cannot refresh OAuth2 token without a refresh token").arg(m_site->url()), Logger::Warning); if (login) { emit loggedIn(Result::Failure); } + m_refreshing = false; return; } + // Set the refresh status and block other refresh requests until this one is completed + m_refreshing = true; + m_refreshForLogin = login; + QNetworkRequest request(m_site->fixUrl(m_auth->tokenUrl())); m_site->setRequestHeaders(request); @@ -326,6 +340,8 @@ void OAuth2Login::refresh(bool login) data = jsonDoc.toJson(); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); } else { + const QString consumerKey = m_settings->value("auth/consumerKey").toString(); + const QString consumerSecret = m_settings->value("auth/consumerSecret").toString(); const QList body { { "grant_type", "refresh_token" }, { "client_id", consumerKey }, @@ -344,16 +360,16 @@ void OAuth2Login::refresh(bool login) // Post request and wait for a reply m_refreshReply = m_manager->post(request, data); - if (login) { - connect(m_refreshReply, &NetworkReply::finished, this, &OAuth2Login::refreshLoginFinished); - } else { - connect(m_refreshReply, &NetworkReply::finished, this, &OAuth2Login::refreshFinished); - } + connect(m_refreshReply, &NetworkReply::finished, this, &OAuth2Login::refreshFinished); } -void OAuth2Login::refreshLoginFinished() +void OAuth2Login::refreshFinished() { const bool ok = readResponse(m_refreshReply); + m_refreshing = false; + if (!m_refreshForLogin) { + return; + } if (!ok) { if (m_auth->authType() == "refresh_token") { log(QStringLiteral("[%1] Refresh failed").arg(m_site->url()), Logger::Warning); @@ -366,15 +382,13 @@ void OAuth2Login::refreshLoginFinished() m_settings->remove("auth/accessToken"); m_refreshToken.clear(); m_settings->remove("auth/refreshToken"); + m_expires = QDateTime(); + m_settings->remove("auth/accessTokenExpiration"); login(); } else { emit loggedIn(Result::Success); } } -void OAuth2Login::refreshFinished() -{ - readResponse(m_refreshReply); -} bool OAuth2Login::readResponse(NetworkReply *reply) { @@ -441,6 +455,7 @@ bool OAuth2Login::readResponse(NetworkReply *reply) const int expiresSecond = QDateTime::currentDateTime().secsTo(m_expires); QTimer::singleShot((expiresSecond / 2) * 1000, this, SIGNAL(basicRefresh())); log(QStringLiteral("[%1] Token will expire at '%2'").arg(m_site->url(), m_expires.toString("yyyy-MM-dd HH:mm:ss")), Logger::Debug); + m_settings->setValue("auth/accessTokenExpiration", m_expires); } } @@ -449,6 +464,11 @@ bool OAuth2Login::readResponse(NetworkReply *reply) void OAuth2Login::complementRequest(QNetworkRequest *request) const { + // Trigger a token refresh in the background if the token is expired + if (!m_refreshToken.isEmpty() && (!m_expires.isValid() || m_expires < QDateTime::currentDateTime())) { + const_cast(this)->refresh(false); + } + if (!m_accessToken.isEmpty()) { request->setRawHeader("Authorization", "Bearer " + m_accessToken.toUtf8()); } diff --git a/src/lib/src/login/oauth2-login.h b/src/lib/src/login/oauth2-login.h index 87c8c54de..3bfd97343 100644 --- a/src/lib/src/login/oauth2-login.h +++ b/src/lib/src/login/oauth2-login.h @@ -31,7 +31,6 @@ class OAuth2Login : public Login protected slots: void loginFinished(); - void refreshLoginFinished(); void refreshFinished(); void basicRefresh(); @@ -54,6 +53,8 @@ class OAuth2Login : public Login QString m_accessToken; QString m_refreshToken; QDateTime m_expires; + bool m_refreshing = false; + bool m_refreshForLogin = false; }; #endif // OAUTH2_LOGIN_H diff --git a/src/lib/vendor/qt-google-analytics/chromium-user-agent.cpp b/src/lib/vendor/qt-google-analytics/chromium-user-agent.cpp new file mode 100644 index 000000000..8021ba3c2 --- /dev/null +++ b/src/lib/vendor/qt-google-analytics/chromium-user-agent.cpp @@ -0,0 +1,177 @@ +#include "chromium-user-agent.h" +#include +#include +#include + +#if defined(Q_OS_ANDROID) + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + #include + #else + #include + typedef QAndroidJniObject QJniObject; + #endif +#endif +#if defined(Q_OS_LINUX) + #include +#endif +#if defined(Q_OS_WIN) + #include +#endif + + +bool IsTablet() +{ + return false; // TODO +} + +bool IsWowX86OnAMD64() +{ + return false; // TODO +} + +// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/common/user_agent.cc;l=43;drc=31d99ff4aa0cc0b75063325ff243e911516a5a6a +QString GetUserAgentPlatform() +{ + #if defined(Q_OS_WIN) + return {}; + #elif defined(Q_OS_MACOS) + return "Macintosh; "; + #elif defined(Q_OS_LINUX) + return "X11; "; + #elif defined(Q_OS_ANDROID) + return "Linux; "; + #elif defined(Q_OS_IOS) + return IsTablet() ? "iPad; " : "iPhone; "; + #endif + return {}; +} + +// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/common/user_agent.cc;l=211;drc=31d99ff4aa0cc0b75063325ff243e911516a5a6a +QString GetOSVersion() +{ + const auto osVersion = QOperatingSystemVersion::current(); + + int os_major_version = osVersion.majorVersion(); + int os_minor_version = osVersion.minorVersion(); + int os_bugfix_version = osVersion.microVersion(); + + #if defined(Q_OS_WIN) + // We don't use GetVersionEx unlike Chromium here + // See https://learn.microsoft.com/en-us/windows/win32/sysinfo/operating-system-version + if (os_major_version == 10 || os_major_version == 11) { + return "10.0"; + } else if (os_major_version == 8 && os_minor_version == 1) { + return "6.3"; + } else if (os_major_version == 8) { + return "6.2"; + } else if (os_major_version == 7) { + return "6.1"; + } + return {}; + #elif defined(Q_OS_MACOS) + if (os_major_version > 10) { + os_major_version = 10; + os_minor_version = 15; + os_bugfix_version = 7; + } + return QString("%1_%2_%3").arg(os_major_version).arg(os_minor_version).arg(os_bugfix_version); + #elif defined(Q_OS_IOS) + return QString("%1_%2").arg(os_major_version).arg(os_minor_version); + #elif defined(Q_OS_ANDROID) + const QString android_version_str = QString("%1.%2.%3%4").arg(os_major_version).arg(os_minor_version).arg(os_bugfix_version); + const QString model = QJniObject::getStaticObjectField("android/os/Build", "MODEL").toString(); + const QString android_info_str = model.isEmpty() ? "" : QString("; %1").arg(model); + return QString("%1%2").arg(android_version_str).arg(android_info_str); + #endif + return {}; +} + +// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/common/user_agent.cc;l=101;drc=31d99ff4aa0cc0b75063325ff243e911516a5a6a +QString BuildCpuInfo() +{ + #if defined(Q_OS_MACOS) + return "Intel"; + #elif defined(Q_OS_IOS) + return IsTablet() ? "iPad" : "iPhone"; + #elif defined(Q_OS_WIN) + + if (IsWowX86OnAMD64()) { + return "WOW64"; + } else { + + _SYSTEM_INFO sysinfo; + GetNativeSystemInfo(&sysinfo); + if (sysinfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64) + return "Win64; x64"; + else if (sysinfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_IA64) + return "Win64; IA64"; + } + #elif defined(Q_OS_LINUX) + struct utsname unixinfo; + uname(&unixinfo); + if (strcmp(unixinfo.machine, "x86_64") == 0 && sizeof(void*) == sizeof(int32_t)) { + return "i686 (x86_64)"; + } else { + return unixinfo.machine; + } + #endif + return {}; +} + +// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/common/user_agent.cc;l=269;drc=31d99ff4aa0cc0b75063325ff243e911516a5a6a +QString BuildOSCpuInfoFromOSVersionAndCpuType(const QString &os_version, const QString &cpu_type) +{ + #if defined(Q_OS_WIN) + if (!cpu_type.isEmpty()) { + return QString("Windows NT %1; %2").arg(os_version, cpu_type); + } else { + return QString("Windows NT %1").arg(os_version); + } + #elif defined(Q_OS_MACOS) + return QString("%1 Mac OS X %2").arg(cpu_type, os_version); + #elif defined(Q_OS_ANDROID) + return QString("Android %1").arg(os_version); + #elif defined(Q_OS_IOS) + return QString("CPU %1 OS %2 like Mac OS X").arg(cpu_type, os_version); + #elif defined(Q_OS_LINUX) + struct utsname unixinfo; + uname(&unixinfo); + return QString("%1 %2").arg(unixinfo.sysname, cpu_type); + #endif + return {}; +} + +// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/common/user_agent.cc;l=261;drc=31d99ff4aa0cc0b75063325ff243e911516a5a6a +QString BuildOSCpuInfo() +{ + return BuildOSCpuInfoFromOSVersionAndCpuType(GetOSVersion(), BuildCpuInfo()); +} + +// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/common/user_agent.cc;l=402;drc=31d99ff4aa0cc0b75063325ff243e911516a5a6a +QString BuildUserAgentFromOSAndProduct(const QString& os_info, const QString& product) +{ + return QString("Mozilla/5.0 (%1) AppleWebKit/537.36 (KHTML, like Gecko) %2 Safari/537.36").arg(os_info, product); +} + +// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/common/user_agent.cc;l=333;drc=31d99ff4aa0cc0b75063325ff243e911516a5a6a +QString BuildUserAgentFromProduct(const QString &product) +{ + const QString platform = GetUserAgentPlatform(); + const QString cpuInfo = BuildOSCpuInfo(); + const QString osInfo = QString("%1%2").arg(platform, cpuInfo); + return BuildUserAgentFromOSAndProduct(osInfo, product); +} + + +QString buildUserAgent() +{ + const QString appName = QCoreApplication::instance()->applicationName(); + const QString appVersion = QCoreApplication::instance()->applicationVersion(); + const QString product = QString("%1/%2").arg(appName, appVersion); + return buildUserAgentForProduct(product); +} + +QString buildUserAgentForProduct(const QString &product) +{ + return BuildUserAgentFromProduct(product); +} diff --git a/src/lib/vendor/qt-google-analytics/chromium-user-agent.h b/src/lib/vendor/qt-google-analytics/chromium-user-agent.h new file mode 100644 index 000000000..d5ef2c66c --- /dev/null +++ b/src/lib/vendor/qt-google-analytics/chromium-user-agent.h @@ -0,0 +1,18 @@ +#ifndef USER_AGENT_H +#define USER_AGENT_H + +#include + + +/** + * User-Agent HTTP header generator based on Chromium implementation. + * https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/common/user_agent.cc + */ +QString buildUserAgent(); + +/** + * Create an User-Agent for the specified product name/version. +*/ +QString buildUserAgentForProduct(const QString &product); + +#endif // USER_AGENT_H diff --git a/src/lib/vendor/qt-google-analytics/qt-google-analytics.cpp b/src/lib/vendor/qt-google-analytics/qt-google-analytics.cpp index 2998cb283..60507b862 100644 --- a/src/lib/vendor/qt-google-analytics/qt-google-analytics.cpp +++ b/src/lib/vendor/qt-google-analytics/qt-google-analytics.cpp @@ -9,13 +9,14 @@ #include #include #include +#include "chromium-user-agent.h" #ifdef QT_GUI_LIB #include #include #endif #ifdef Q_OS_ANDROID -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #include #else #include @@ -26,6 +27,7 @@ #define MEASUREMENT_ENDPOINT_WEB "https://www.google-analytics.com/g/collect" #define CLIENT_ID_SETTINGS_KEY "QtGoogleAnalytics/ClientId" #define SESSION_START_INTERVAL_SECONDS 1800 +#define USER_AGENT_PRODUCT "Chrome/114.0.0.0" #include "functions.h" @@ -43,6 +45,8 @@ QtGoogleAnalytics::QtGoogleAnalytics(QObject *parent) } else { m_clientId = settings.value(CLIENT_ID_SETTINGS_KEY).toString(); } + + m_userAgent = userAgent(); } QtGoogleAnalytics::QtGoogleAnalytics(const QString &measurementId, QObject *parent) @@ -170,7 +174,8 @@ void QtGoogleAnalytics::sendEvent(const QString &name, const QVariantMap ¶me url.setQuery(query); QNetworkRequest request(url); - request.setHeader(QNetworkRequest::UserAgentHeader, userAgent().toLatin1()); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + request.setHeader(QNetworkRequest::UserAgentHeader, m_userAgent); m_uach.setRequestHeaders(request); QNetworkReply *reply = m_networkAccessManager->post(request, QByteArray()); @@ -196,22 +201,16 @@ void QtGoogleAnalytics::sendEvent(const QString &name, const QVariantMap ¶me QString QtGoogleAnalytics::userAgent() const { - return ""; - #if defined(Q_OS_ANDROID) // On Android, just use System.getProperty("http.agent") - QAndroidJniObject ua = QJniObject::callStaticMethod( + return QJniObject::callStaticObjectMethod( "System", "getProperty", "(Ljava/lang/String;)Z", QJniObject::fromString("http.agent").object() - ); - return ua.toString(); + ).toString(); #endif - // On other platforms, use a custom User-Agent - const QString appName = QCoreApplication::instance()->applicationName(); - const QString appVersion = QCoreApplication::instance()->applicationVersion(); - const QString systemInfo = QString("%1 %2").arg(QOperatingSystemVersion::current().name(), m_uach.platformVersion()); - return QString("%1/%2 (%3) QtGoogleAnalytics/1.0 (Qt/%4)").arg(appName, appVersion, systemInfo, QT_VERSION_STR); + // On other platforms, use a Chrome User-Agent + return buildUserAgentForProduct(USER_AGENT_PRODUCT); } diff --git a/src/lib/vendor/qt-google-analytics/qt-google-analytics.h b/src/lib/vendor/qt-google-analytics/qt-google-analytics.h index 04ab8eb28..bebb0fc04 100644 --- a/src/lib/vendor/qt-google-analytics/qt-google-analytics.h +++ b/src/lib/vendor/qt-google-analytics/qt-google-analytics.h @@ -50,6 +50,7 @@ class QtGoogleAnalytics : public QObject private: QNetworkAccessManager *m_networkAccessManager; UserAgentClientHints m_uach; + QString m_userAgent; unsigned int m_sessionId; QDateTime m_lastEvent; diff --git a/src/lib/vendor/qt-google-analytics/user-agent-client-hints.cpp b/src/lib/vendor/qt-google-analytics/user-agent-client-hints.cpp index c7cf4f46a..19e4964d5 100644 --- a/src/lib/vendor/qt-google-analytics/user-agent-client-hints.cpp +++ b/src/lib/vendor/qt-google-analytics/user-agent-client-hints.cpp @@ -5,7 +5,7 @@ #include #ifdef Q_OS_ANDROID -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #include #else #include diff --git a/src/sites/FurAffinity/model.ts b/src/sites/FurAffinity/model.ts index 86ae0b5d1..864872a26 100644 --- a/src/sites/FurAffinity/model.ts +++ b/src/sites/FurAffinity/model.ts @@ -15,6 +15,7 @@ export const source: ISource = { name: "Regex", auth: [], forcedLimit: 24, + forcedTokens: ["*"], search: { parseErrors: true, url: (query: ISearchQuery): string | IError => { diff --git a/src/sites/Kemono/model.ts b/src/sites/Kemono/model.ts index 1f3061b18..091ac3fca 100644 --- a/src/sites/Kemono/model.ts +++ b/src/sites/Kemono/model.ts @@ -58,7 +58,7 @@ export const source: ISource = { auth: [], maxLimit: 50, search: { - url: (query: ISearchQuery, opts: IUrlOptions, previous: IPreviousSearch | undefined): string | IError => { + url: (query: ISearchQuery, opts: IUrlOptions): string | IError => { const offset = (query.page - 1) * opts.limit; if (query.search) { return {error: "The JSON API does not support arbitrary search."}; @@ -114,5 +114,57 @@ export const source: ISource = { }, }, }, + html: { + name: "Regex", + auth: [], + forcedLimit: 50, + search: { + url: (query: ISearchQuery, opts: IUrlOptions): string | IError => { + const offset = (query.page - 1) * opts.limit; + return "/posts?o=" + offset + "&q=" + encodeURIComponent(query.search); + }, + parse: (src: string): IParsedSearch | IError => { + const html = Grabber.parseHTML(src); + const articles = html.find("article.post-card"); + + const images: IImage[] = []; + for (const article of articles) { + // Basic attributes + const identity = { + service: article.attr("data-service"), + user: article.attr("data-user"), + id: article.attr("data-id"), + }; + const image: IImage = { + identity, + id: identity["id"], + author_id: identity["user"], + name: article.find("header")[0].innerText().trim(), + created_at: article.find("time")[0].attr("datetime"), + }; + + // Not all posts have an image + const img = article.find("img"); + if (img.length > 0) { + image.preview_url = img[0].attr("src"); + } + + // Detect galleries with multiple files + const attachmentCount = parseInt(Grabber.regexToConst("count", "(?\\d+) attachments?", article.innerHTML()), 10) + if (attachmentCount > 1) { + image.type = "gallery"; + image.gallery_count = attachmentCount; + } + + images.push(image); + } + + return { + images, + imageCount: Grabber.regexToConst("count", "Showing \\d+ - \\d+ of (?\\d+)", src), + }; + }, + }, + }, }, };