From 62a911f8ef17d3a59b0602cb1da2f4a35645c083 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sun, 1 Feb 2026 17:11:06 +0100 Subject: [PATCH 01/11] Add web cache feature for automatic asset downloading Adds a web cache system that automatically downloads missing assets from the server's asset URL and caches them locally. Includes options UI for enabling/ disabling the cache, setting expiry time, viewing cache size, and clearing. Also fixes macOS build issues: - Override WrapOpenGL to skip deprecated AGL framework - Fix BASS/BASSOPUS header paths in configure.sh for macOS - Fix vexing parse in webcache.cpp Co-Authored-By: Claude Opus 4.5 --- CMakeLists.txt | 11 +- configure.sh | 4 +- data/ui/options_dialog.ui | 93 +++++++++++++ src/aoapplication.cpp | 7 + src/aoapplication.h | 7 + src/options.cpp | 20 +++ src/options.h | 7 + src/path_functions.cpp | 15 +++ src/webcache.cpp | 228 ++++++++++++++++++++++++++++++++ src/webcache.h | 81 ++++++++++++ src/widgets/aooptionsdialog.cpp | 42 ++++++ src/widgets/aooptionsdialog.h | 7 + 12 files changed, 519 insertions(+), 3 deletions(-) create mode 100644 src/webcache.cpp create mode 100644 src/webcache.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ebaaae253..16992fa27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.7.0) +cmake_minimum_required(VERSION 3.16) project(AttorneyOnline VERSION 2.11.0.0 LANGUAGES CXX C) @@ -14,6 +14,13 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) option(AO_BUILD_TESTS "Build test programs" ON) option(AO_ENABLE_DISCORD_RPC "Enable Discord Rich Presence" ON) +# Override WrapOpenGL to not require deprecated AGL framework on macOS +if(APPLE AND NOT TARGET WrapOpenGL::WrapOpenGL) + add_library(WrapOpenGL::WrapOpenGL INTERFACE IMPORTED) + target_link_libraries(WrapOpenGL::WrapOpenGL INTERFACE "-framework OpenGL") + set(WrapOpenGL_FOUND ON) +endif() + find_package(QT NAMES Qt6) find_package(Qt6 REQUIRED COMPONENTS Core Gui Network Widgets Concurrent WebSockets UiTools) @@ -91,6 +98,8 @@ qt_add_executable(Attorney_Online src/serverdata.cpp src/serverdata.h src/text_file_functions.cpp + src/webcache.cpp + src/webcache.h src/widgets/aooptionsdialog.cpp src/widgets/aooptionsdialog.h src/widgets/direct_connect_dialog.cpp diff --git a/configure.sh b/configure.sh index 619f1bf2d..3ffa68e43 100755 --- a/configure.sh +++ b/configure.sh @@ -240,7 +240,7 @@ get_bass() { libs/x86_64/libbass.so:./bin elif [[ "$PLATFORM" == "macos" ]]; then get_zip https://www.un4seen.com/files/bass24-osx.zip \ - bass.h:./lib \ + c/bass.h:./lib \ libbass.dylib:./lib fi } @@ -266,7 +266,7 @@ get_bassopus() { libs/x86_64/libbassopus.so:./bin elif [[ "$PLATFORM" == "macos" ]]; then get_zip https://www.un4seen.com/files/bassopus24-osx.zip \ - bassopus.h:./lib \ + c/bassopus.h:./lib \ libbassopus.dylib:./lib fi } diff --git a/data/ui/options_dialog.ui b/data/ui/options_dialog.ui index 99b60a4f0..c2718c930 100644 --- a/data/ui/options_dialog.ui +++ b/data/ui/options_dialog.ui @@ -908,6 +908,99 @@ Use this when you have added an asset that takes precedence over another existin + + + + QFrame::HLine + + + QFrame::Sunken + + + + + + + Web Cache + + + + true + + + + + + + + + + If enabled, assets not found locally will be automatically downloaded from the server's asset URL and cached for future use. + + + Enable Web Cache: + + + + + + + + + + + + + + How long cached files remain valid before being re-downloaded. Set to a higher value to reduce bandwidth usage. + + + Cache Expiry: + + + + + + + hours + + + 1 + + + 720 + + + 24 + + + + + + + Cache Size: + + + + + + + 0 MB + + + + + + + Deletes all cached files downloaded from servers. + + + Clear Web Cache + + + + + diff --git a/src/aoapplication.cpp b/src/aoapplication.cpp index 774816bbf..2817999c3 100644 --- a/src/aoapplication.cpp +++ b/src/aoapplication.cpp @@ -5,6 +5,7 @@ #include "lobby.h" #include "networkmanager.h" #include "options.h" +#include "webcache.h" #include "widgets/aooptionsdialog.h" static QtMessageHandler original_message_handler; @@ -21,6 +22,7 @@ AOApplication::AOApplication(QObject *parent) { net_manager = new NetworkManager(this); discord = new AttorneyOnline::Discord(); + m_webcache = new WebCache(this); asset_lookup_cache.reserve(2048); @@ -36,6 +38,11 @@ AOApplication::~AOApplication() qInstallMessageHandler(original_message_handler); } +WebCache *AOApplication::webcache() const +{ + return m_webcache; +} + bool AOApplication::is_lobby_constructed() { return w_lobby; diff --git a/src/aoapplication.h b/src/aoapplication.h index 5e67fc862..e16b8e40e 100644 --- a/src/aoapplication.h +++ b/src/aoapplication.h @@ -29,6 +29,7 @@ class NetworkManager; class Lobby; class Courtroom; class Options; +class WebCache; class VPath : QString { @@ -60,6 +61,12 @@ class AOApplication : public QObject Lobby *w_lobby = nullptr; Courtroom *w_courtroom = nullptr; AttorneyOnline::Discord *discord; + WebCache *webcache() const; + +private: + WebCache *m_webcache = nullptr; + +public: QFont default_font; diff --git a/src/options.cpp b/src/options.cpp index caa559c80..95cc92d71 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -794,3 +794,23 @@ void Options::setRestoreWindowPositionEnabled(bool state) { config.setValue("windows/restore", state); } + +bool Options::webcacheEnabled() const +{ + return config.value("webcache_enabled", true).toBool(); +} + +void Options::setWebcacheEnabled(bool value) +{ + config.setValue("webcache_enabled", value); +} + +int Options::webcacheExpiryHours() const +{ + return config.value("webcache_expiry_hours", 24).toInt(); +} + +void Options::setWebcacheExpiryHours(int hours) +{ + config.setValue("webcache_expiry_hours", hours); +} diff --git a/src/options.h b/src/options.h index 58b02253b..52fd1132a 100644 --- a/src/options.h +++ b/src/options.h @@ -281,6 +281,13 @@ class Options bool restoreWindowPositionEnabled() const; void setRestoreWindowPositionEnabled(bool state); + // Webcache settings + bool webcacheEnabled() const; + void setWebcacheEnabled(bool value); + + int webcacheExpiryHours() const; + void setWebcacheExpiryHours(int hours); + private: /** * @brief QSettings object for config.ini diff --git a/src/path_functions.cpp b/src/path_functions.cpp index 9b969c489..56a1c2324 100644 --- a/src/path_functions.cpp +++ b/src/path_functions.cpp @@ -2,6 +2,7 @@ #include "courtroom.h" #include "file_functions.h" #include "options.h" +#include "webcache.h" #include #include @@ -421,6 +422,20 @@ QString AOApplication::get_real_path(const VPath &vpath, const QStringList &suff } } + // Check webcache if local file not found + if (Options::getInstance().webcacheEnabled() && !m_serverdata.get_asset_url().isEmpty()) + { + QString cached = m_webcache->getCachedPath(vpath.toQString()); + if (!cached.isEmpty()) + { + asset_lookup_cache.insert(qHash(vpath), cached); + return cached; + } + + // Initiate background download for future requests + m_webcache->resolveOrDownload(vpath.toQString(), suffixes); + } + // Not found in mount paths; check if the file is remote QString remotePath = vpath.toQString(); if (remotePath.startsWith("http:") || remotePath.startsWith("https:")) diff --git a/src/webcache.cpp b/src/webcache.cpp new file mode 100644 index 000000000..67df87749 --- /dev/null +++ b/src/webcache.cpp @@ -0,0 +1,228 @@ +#include "webcache.h" + +#include "aoapplication.h" +#include "file_functions.h" +#include "options.h" + +#include +#include +#include +#include +#include + +WebCache::WebCache(AOApplication *parent) + : QObject(parent) + , ao_app(parent) + , m_network_manager(new QNetworkAccessManager(this)) +{ + connect(m_network_manager, &QNetworkAccessManager::finished, this, &WebCache::onDownloadFinished); +} + +WebCache::~WebCache() +{ +} + +QString WebCache::cacheDir() const +{ + return get_base_path() + "webcache/"; +} + +QString WebCache::getCachedPath(const QString &relativePath) const +{ + if (relativePath.isEmpty()) + { + return QString(); + } + + QString localPath = cacheDir() + relativePath; + + if (!file_exists(localPath)) + { + return QString(); + } + + if (isExpired(localPath)) + { + return QString(); + } + + return localPath; +} + +bool WebCache::isExpired(const QString &localPath) const +{ + QFileInfo fileInfo(localPath); + if (!fileInfo.exists()) + { + return true; + } + + int expiryHours = Options::getInstance().webcacheExpiryHours(); + QDateTime expiryTime = fileInfo.lastModified().addSecs(expiryHours * 3600); + + return QDateTime::currentDateTime() > expiryTime; +} + +void WebCache::resolveOrDownload(const QString &relativePath, const QStringList &suffixes) +{ + if (relativePath.isEmpty()) + { + return; + } + + // Check if webcache is enabled + if (!Options::getInstance().webcacheEnabled()) + { + return; + } + + // Check if server has an asset URL + QString assetUrl = ao_app->m_serverdata.get_asset_url(); + if (assetUrl.isEmpty()) + { + return; + } + + // Ensure asset URL ends with / + if (!assetUrl.endsWith('/')) + { + assetUrl += '/'; + } + + // Check if already downloading this path + if (m_pending_downloads.contains(relativePath)) + { + return; + } + + // Try each suffix + for (const QString &suffix : suffixes) + { + QString fullPath = relativePath + suffix; + QString localPath = cacheDir() + fullPath; + + // Skip if already cached and not expired + if (file_exists(localPath) && !isExpired(localPath)) + { + continue; + } + + QString remoteUrl = assetUrl + fullPath; + + // Mark as pending and start download + m_pending_downloads.insert(relativePath, true); + startDownload(remoteUrl, localPath, relativePath); + return; // Only try one suffix at a time + } +} + +void WebCache::startDownload(const QString &remoteUrl, const QString &localPath, const QString &relativePath) +{ + QNetworkRequest request{QUrl(remoteUrl)}; + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + + // Store the local path and relative path in the request for later retrieval + request.setAttribute(QNetworkRequest::User, localPath); + request.setAttribute(static_cast(QNetworkRequest::User + 1), relativePath); + + qDebug() << "WebCache: Downloading" << remoteUrl; + m_network_manager->get(request); +} + +void WebCache::onDownloadFinished(QNetworkReply *reply) +{ + QString localPath = reply->request().attribute(QNetworkRequest::User).toString(); + QString relativePath = reply->request().attribute(static_cast(QNetworkRequest::User + 1)).toString(); + + // Remove from pending + m_pending_downloads.remove(relativePath); + + if (reply->error() != QNetworkReply::NoError) + { + qDebug() << "WebCache: Download failed for" << reply->url().toString() << "-" << reply->errorString(); + reply->deleteLater(); + return; + } + + // Check HTTP status code + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (statusCode != 200) + { + qDebug() << "WebCache: Download returned status" << statusCode << "for" << reply->url().toString(); + reply->deleteLater(); + return; + } + + // Create directory structure + QFileInfo fileInfo(localPath); + QDir dir = fileInfo.absoluteDir(); + if (!dir.exists()) + { + if (!dir.mkpath(".")) + { + qWarning() << "WebCache: Failed to create directory" << dir.absolutePath(); + reply->deleteLater(); + return; + } + } + + // Write the file + QFile file(localPath); + if (!file.open(QIODevice::WriteOnly)) + { + qWarning() << "WebCache: Failed to open file for writing:" << localPath; + reply->deleteLater(); + return; + } + + file.write(reply->readAll()); + file.close(); + + qDebug() << "WebCache: Successfully cached" << localPath; + reply->deleteLater(); +} + +void WebCache::clearCache() +{ + QDir dir(cacheDir()); + if (dir.exists()) + { + if (!dir.removeRecursively()) + { + qWarning() << "WebCache: Failed to clear cache directory"; + } + else + { + qDebug() << "WebCache: Cache cleared"; + } + } +} + +qint64 WebCache::getCacheSize() const +{ + QDir dir(cacheDir()); + if (!dir.exists()) + { + return 0; + } + return calculateDirSize(dir); +} + +qint64 WebCache::calculateDirSize(const QDir &dir) const +{ + qint64 size = 0; + + QFileInfoList fileList = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + for (const QFileInfo &fileInfo : fileList) + { + size += fileInfo.size(); + } + + QFileInfoList dirList = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo &dirInfo : dirList) + { + size += calculateDirSize(QDir(dirInfo.absoluteFilePath())); + } + + return size; +} diff --git a/src/webcache.h b/src/webcache.h new file mode 100644 index 000000000..2dc6bd14e --- /dev/null +++ b/src/webcache.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class AOApplication; + +class WebCache : public QObject +{ + Q_OBJECT + +public: + explicit WebCache(AOApplication *parent); + ~WebCache(); + + /** + * @brief Returns the cached file path if it exists and is not expired. + * @param relativePath The virtual path relative to the base (e.g., "sounds/music/song.mp3") + * @return The absolute path to the cached file, or empty string if not cached/expired. + */ + QString getCachedPath(const QString &relativePath) const; + + /** + * @brief Check cache and initiate async download if file is missing or expired. + * @param relativePath The virtual path relative to the base. + * @param suffixes List of file extensions to try (e.g., {"", ".opus", ".wav"}) + */ + void resolveOrDownload(const QString &relativePath, const QStringList &suffixes = {""}); + + /** + * @brief Clears the entire webcache directory. + */ + void clearCache(); + + /** + * @brief Returns the total size of the webcache directory in bytes. + */ + qint64 getCacheSize() const; + + /** + * @brief Checks if a cached file has expired based on its modification time. + * @param localPath The absolute path to the cached file. + * @return True if the file is expired, false otherwise. + */ + bool isExpired(const QString &localPath) const; + + /** + * @brief Returns the webcache directory path. + */ + QString cacheDir() const; + +private Q_SLOTS: + void onDownloadFinished(QNetworkReply *reply); + +private: + AOApplication *ao_app; + QNetworkAccessManager *m_network_manager; + + // Track pending downloads to avoid duplicate requests + // Key: relative path, Value: list of suffixes being tried + QHash m_pending_downloads; + + /** + * @brief Initiates an async download for the given remote URL. + * @param remoteUrl The full URL to download from. + * @param localPath The local path where the file should be saved. + * @param relativePath The relative path key for tracking pending downloads. + */ + void startDownload(const QString &remoteUrl, const QString &localPath, const QString &relativePath); + + /** + * @brief Calculates directory size recursively. + */ + qint64 calculateDirSize(const QDir &dir) const; +}; diff --git a/src/widgets/aooptionsdialog.cpp b/src/widgets/aooptionsdialog.cpp index 67f9c1cab..9a4e53419 100644 --- a/src/widgets/aooptionsdialog.cpp +++ b/src/widgets/aooptionsdialog.cpp @@ -6,6 +6,7 @@ #include "gui_utils.h" #include "networkmanager.h" #include "options.h" +#include "webcache.h" #include @@ -538,6 +539,22 @@ void AOOptionsDialog::setupUI() } }); + // Webcache controls + FROM_UI(QCheckBox, webcache_enabled_cb); + FROM_UI(QSpinBox, webcache_expiry_spinbox); + FROM_UI(QPushButton, webcache_clear); + FROM_UI(QLabel, webcache_size_label); + + registerOption("webcache_enabled_cb", &Options::webcacheEnabled, &Options::setWebcacheEnabled); + registerOption("webcache_expiry_spinbox", &Options::webcacheExpiryHours, &Options::setWebcacheExpiryHours); + + connect(ui_webcache_clear, &QPushButton::clicked, this, [this] { + ao_app->webcache()->clearCache(); + updateWebcacheSizeLabel(); + }); + + updateWebcacheSizeLabel(); + // Logging tab FROM_UI(QCheckBox, downwards_cb); FROM_UI(QSpinBox, length_spinbox); @@ -609,6 +626,31 @@ void AOOptionsDialog::timestampCbChanged(int state) ui_log_timestamp_format_combobox->setDisabled(state == 0); } +void AOOptionsDialog::updateWebcacheSizeLabel() +{ + qint64 sizeBytes = ao_app->webcache()->getCacheSize(); + QString sizeStr; + + if (sizeBytes < 1024) + { + sizeStr = QString::number(sizeBytes) + " B"; + } + else if (sizeBytes < 1024 * 1024) + { + sizeStr = QString::number(sizeBytes / 1024.0, 'f', 1) + " KB"; + } + else if (sizeBytes < 1024 * 1024 * 1024) + { + sizeStr = QString::number(sizeBytes / (1024.0 * 1024.0), 'f', 1) + " MB"; + } + else + { + sizeStr = QString::number(sizeBytes / (1024.0 * 1024.0 * 1024.0), 'f', 2) + " GB"; + } + + ui_webcache_size_label->setText(sizeStr); +} + #if (defined(_WIN32) || defined(_WIN64)) bool AOOptionsDialog::needsDefaultAudioDevice() { diff --git a/src/widgets/aooptionsdialog.h b/src/widgets/aooptionsdialog.h index 4409c9eff..e94f29771 100644 --- a/src/widgets/aooptionsdialog.h +++ b/src/widgets/aooptionsdialog.h @@ -102,6 +102,12 @@ class AOOptionsDialog : public QDialog QPushButton *ui_mount_down; QPushButton *ui_mount_clear_cache; + // Webcache controls + QCheckBox *ui_webcache_enabled_cb; + QSpinBox *ui_webcache_expiry_spinbox; + QPushButton *ui_webcache_clear; + QLabel *ui_webcache_size_label; + // The logging tab QCheckBox *ui_downwards_cb; QSpinBox *ui_length_spinbox; @@ -128,6 +134,7 @@ class AOOptionsDialog : public QDialog bool needsDefaultAudioDevice(); void populateAudioDevices(); void updateValues(); + void updateWebcacheSizeLabel(); QVector optionEntries; From f524041151b566790ccf98025f9dc6b35f262f19 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sun, 1 Feb 2026 19:18:02 +0100 Subject: [PATCH 02/11] Fix webcache download loop and reject malformed paths - Track failed downloads to avoid retrying them infinitely within a session - Reject paths containing absolute paths (starting with /, containing //, or :/) to prevent malformed URLs like "evidence//Users/..." Co-Authored-By: Claude Opus 4.5 --- src/webcache.cpp | 15 +++++++++++++++ src/webcache.h | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/src/webcache.cpp b/src/webcache.cpp index 67df87749..d64a349c9 100644 --- a/src/webcache.cpp +++ b/src/webcache.cpp @@ -70,6 +70,13 @@ void WebCache::resolveOrDownload(const QString &relativePath, const QStringList return; } + // Reject paths containing absolute paths (they shouldn't be passed to webcache) + // Check for Unix absolute paths embedded anywhere, or Windows drive letters + if (relativePath.startsWith('/') || relativePath.contains("//") || relativePath.contains(":/")) + { + return; + } + // Check if webcache is enabled if (!Options::getInstance().webcacheEnabled()) { @@ -95,6 +102,12 @@ void WebCache::resolveOrDownload(const QString &relativePath, const QStringList return; } + // Check if this path previously failed (don't retry within this session) + if (m_failed_downloads.contains(relativePath)) + { + return; + } + // Try each suffix for (const QString &suffix : suffixes) { @@ -140,6 +153,7 @@ void WebCache::onDownloadFinished(QNetworkReply *reply) if (reply->error() != QNetworkReply::NoError) { qDebug() << "WebCache: Download failed for" << reply->url().toString() << "-" << reply->errorString(); + m_failed_downloads.insert(relativePath); reply->deleteLater(); return; } @@ -149,6 +163,7 @@ void WebCache::onDownloadFinished(QNetworkReply *reply) if (statusCode != 200) { qDebug() << "WebCache: Download returned status" << statusCode << "for" << reply->url().toString(); + m_failed_downloads.insert(relativePath); reply->deleteLater(); return; } diff --git a/src/webcache.h b/src/webcache.h index 2dc6bd14e..a3d0d46e5 100644 --- a/src/webcache.h +++ b/src/webcache.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -66,6 +67,9 @@ private Q_SLOTS: // Key: relative path, Value: list of suffixes being tried QHash m_pending_downloads; + // Track failed downloads to avoid retrying them repeatedly + QSet m_failed_downloads; + /** * @brief Initiates an async download for the given remote URL. * @param remoteUrl The full URL to download from. From c4ef658dcb231db208df6afaec8a96378ff73f7d Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sun, 1 Feb 2026 19:21:47 +0100 Subject: [PATCH 03/11] Cache assets by server domain to prevent conflicts Assets from different servers are now cached in separate subdirectories based on the asset URL's host and path (e.g., direct.grave.wine/base/). This prevents cache conflicts when connecting to different servers. Co-Authored-By: Claude Opus 4.5 --- src/webcache.cpp | 38 ++++++++++++++++++++++++++++++++++++-- src/webcache.h | 6 ++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/webcache.cpp b/src/webcache.cpp index d64a349c9..ee7de998f 100644 --- a/src/webcache.cpp +++ b/src/webcache.cpp @@ -27,6 +27,27 @@ QString WebCache::cacheDir() const return get_base_path() + "webcache/"; } +QString WebCache::cacheSubdir() const +{ + QString assetUrl = ao_app->m_serverdata.get_asset_url(); + if (assetUrl.isEmpty()) + { + return QString(); + } + + // Extract host and path from asset URL (e.g., "https://direct.grave.wine/base/" -> "direct.grave.wine/base/") + QUrl url(assetUrl); + QString subdir = url.host() + url.path(); + + // Ensure it ends with / + if (!subdir.endsWith('/')) + { + subdir += '/'; + } + + return subdir; +} + QString WebCache::getCachedPath(const QString &relativePath) const { if (relativePath.isEmpty()) @@ -34,7 +55,13 @@ QString WebCache::getCachedPath(const QString &relativePath) const return QString(); } - QString localPath = cacheDir() + relativePath; + QString subdir = cacheSubdir(); + if (subdir.isEmpty()) + { + return QString(); + } + + QString localPath = cacheDir() + subdir + relativePath; if (!file_exists(localPath)) { @@ -108,11 +135,18 @@ void WebCache::resolveOrDownload(const QString &relativePath, const QStringList return; } + // Get cache subdirectory for this server's asset URL + QString subdir = cacheSubdir(); + if (subdir.isEmpty()) + { + return; + } + // Try each suffix for (const QString &suffix : suffixes) { QString fullPath = relativePath + suffix; - QString localPath = cacheDir() + fullPath; + QString localPath = cacheDir() + subdir + fullPath; // Skip if already cached and not expired if (file_exists(localPath) && !isExpired(localPath)) diff --git a/src/webcache.h b/src/webcache.h index a3d0d46e5..1cdad784c 100644 --- a/src/webcache.h +++ b/src/webcache.h @@ -70,6 +70,12 @@ private Q_SLOTS: // Track failed downloads to avoid retrying them repeatedly QSet m_failed_downloads; + /** + * @brief Returns the cache subdirectory for the current server's asset URL. + * E.g., "https://direct.grave.wine/base/" -> "direct.grave.wine/base/" + */ + QString cacheSubdir() const; + /** * @brief Initiates an async download for the given remote URL. * @param remoteUrl The full URL to download from. From a86516db40077ca6587402f9d3e6996f8278f07f Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sun, 1 Feb 2026 19:48:09 +0100 Subject: [PATCH 04/11] Match webAO URL lookup behavior: lowercase paths and encodeURI encoding - Add normalizePathForWeb() to lowercase and percent-encode path components - Use JavaScript encodeURI-compatible encoding (preserves safe chars like ! ' ( ) *) - Check pending/failed downloads per normalized path with suffix - Return early if cached file exists for any suffix Co-Authored-By: Claude Opus 4.5 --- src/webcache.cpp | 75 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/src/webcache.cpp b/src/webcache.cpp index ee7de998f..2fd1af177 100644 --- a/src/webcache.cpp +++ b/src/webcache.cpp @@ -10,6 +10,33 @@ #include #include +namespace { +// Normalize path like webAO: lowercase and URL-encode each component +// Uses encodeURI-compatible encoding (preserves safe characters like ! ' ( ) *) +QString normalizePathForWeb(const QString &path) +{ + // Split path into components, lowercase and URL-encode each, then rejoin + QStringList components = path.split('/'); + QStringList encoded; + // Characters that encodeURI does NOT encode (excluding / which we handle via split) + // These are: A-Za-z0-9 ; , ? : @ & = + $ - _ . ! ~ * ' ( ) # + // We only pass the non-alphanumeric ones since alphanumerics are never encoded + const QByteArray safeChars = ";,?:@&=+$-_.!~*'()#"; + for (const QString &component : components) + { + if (component.isEmpty()) + { + continue; + } + // Lowercase and URL-encode (percent-encode) the component + QString lowered = component.toLower(); + QString percentEncoded = QUrl::toPercentEncoding(lowered, safeChars); + encoded.append(percentEncoded); + } + return encoded.join('/'); +} +} // namespace + WebCache::WebCache(AOApplication *parent) : QObject(parent) , ao_app(parent) @@ -61,7 +88,9 @@ QString WebCache::getCachedPath(const QString &relativePath) const return QString(); } - QString localPath = cacheDir() + subdir + relativePath; + // Use normalized (lowercase) path for cache lookup + QString normalizedPath = normalizePathForWeb(relativePath); + QString localPath = cacheDir() + subdir + normalizedPath; if (!file_exists(localPath)) { @@ -123,18 +152,6 @@ void WebCache::resolveOrDownload(const QString &relativePath, const QStringList assetUrl += '/'; } - // Check if already downloading this path - if (m_pending_downloads.contains(relativePath)) - { - return; - } - - // Check if this path previously failed (don't retry within this session) - if (m_failed_downloads.contains(relativePath)) - { - return; - } - // Get cache subdirectory for this server's asset URL QString subdir = cacheSubdir(); if (subdir.isEmpty()) @@ -142,23 +159,41 @@ void WebCache::resolveOrDownload(const QString &relativePath, const QStringList return; } - // Try each suffix - for (const QString &suffix : suffixes) + // Try each suffix (like webAO tries multiple extensions) + QStringList effectiveSuffixes = suffixes.isEmpty() ? QStringList{""} : suffixes; + for (const QString &suffix : effectiveSuffixes) { QString fullPath = relativePath + suffix; - QString localPath = cacheDir() + subdir + fullPath; + + // Normalize path like webAO: lowercase and URL-encode + QString normalizedPath = normalizePathForWeb(fullPath); + + // Check if already downloading this path + if (m_pending_downloads.contains(normalizedPath)) + { + return; + } + + // Check if this path previously failed (don't retry within this session) + if (m_failed_downloads.contains(normalizedPath)) + { + continue; // Try next suffix + } + + QString localPath = cacheDir() + subdir + normalizedPath; // Skip if already cached and not expired if (file_exists(localPath) && !isExpired(localPath)) { - continue; + return; // Already have a valid cached file } - QString remoteUrl = assetUrl + fullPath; + // Construct remote URL with normalized path + QString remoteUrl = assetUrl + normalizedPath; // Mark as pending and start download - m_pending_downloads.insert(relativePath, true); - startDownload(remoteUrl, localPath, relativePath); + m_pending_downloads.insert(normalizedPath, true); + startDownload(remoteUrl, localPath, normalizedPath); return; // Only try one suffix at a time } } From b13412826d43a4536b1865cb9df41756ead0664e Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sun, 1 Feb 2026 19:52:10 +0100 Subject: [PATCH 05/11] Apply clang-format to webcache files Co-Authored-By: Claude Opus 4.5 --- src/webcache.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webcache.cpp b/src/webcache.cpp index 2fd1af177..621122c1c 100644 --- a/src/webcache.cpp +++ b/src/webcache.cpp @@ -10,7 +10,8 @@ #include #include -namespace { +namespace +{ // Normalize path like webAO: lowercase and URL-encode each component // Uses encodeURI-compatible encoding (preserves safe characters like ! ' ( ) *) QString normalizePathForWeb(const QString &path) @@ -46,8 +47,7 @@ WebCache::WebCache(AOApplication *parent) } WebCache::~WebCache() -{ -} +{} QString WebCache::cacheDir() const { From 16d5f9b48f2d401572845534aa71c00e0b8cd6b1 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sun, 1 Feb 2026 19:53:34 +0100 Subject: [PATCH 06/11] puzzling whitespace --- src/aoapplication.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/aoapplication.h b/src/aoapplication.h index e16b8e40e..3bf9e4c2f 100644 --- a/src/aoapplication.h +++ b/src/aoapplication.h @@ -67,7 +67,6 @@ class AOApplication : public QObject WebCache *m_webcache = nullptr; public: - QFont default_font; bool is_lobby_constructed(); From 025626f61bffd3fa93f432b5210be8f041c423d5 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sun, 1 Feb 2026 20:04:05 +0100 Subject: [PATCH 07/11] thanks bass --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 031cd4fa4..7311c5fd4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -139,13 +139,13 @@ jobs: run: | curl http://www.un4seen.com/files/bass24-linux.zip -o bass.zip unzip -d bass -o bass.zip - cp ./bass/bass.h ./lib + cp ./bass/c/bass.h ./lib cp ./bass/libs/x86_64/libbass.so ./lib/ cp ./bass/libs/x86_64/libbass.so ./bin/ curl http://www.un4seen.com/files/bassopus24-linux.zip -o bassopus.zip unzip -d bass -o bassopus.zip - cp ./bass/bassopus.h ./lib + cp ./bass/c/bassopus.h ./lib cp ./bass/libs/x86_64/libbassopus.so ./lib/ cp ./bass/libs/x86_64/libbassopus.so ./bin/ From 5a2274cf45c031c7e792aea6ff6ffe0559d605a8 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sun, 1 Feb 2026 20:05:59 +0100 Subject: [PATCH 08/11] Store cached files without percent-encoding in paths - Separate lowercasePath() for local storage (no encoding) - Use urlEncodePath() only for remote HTTP requests - Local paths now have actual spaces and special characters Co-Authored-By: Claude Opus 4.5 --- src/webcache.cpp | 51 ++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/webcache.cpp b/src/webcache.cpp index 621122c1c..d7ae9e5b0 100644 --- a/src/webcache.cpp +++ b/src/webcache.cpp @@ -12,16 +12,30 @@ namespace { -// Normalize path like webAO: lowercase and URL-encode each component +// Lowercase path components (for local storage) +QString lowercasePath(const QString &path) +{ + QStringList components = path.split('/'); + QStringList result; + for (const QString &component : components) + { + if (component.isEmpty()) + { + continue; + } + result.append(component.toLower()); + } + return result.join('/'); +} + +// URL-encode path for web requests (like webAO's encodeURI) // Uses encodeURI-compatible encoding (preserves safe characters like ! ' ( ) *) -QString normalizePathForWeb(const QString &path) +QString urlEncodePath(const QString &path) { - // Split path into components, lowercase and URL-encode each, then rejoin QStringList components = path.split('/'); QStringList encoded; // Characters that encodeURI does NOT encode (excluding / which we handle via split) // These are: A-Za-z0-9 ; , ? : @ & = + $ - _ . ! ~ * ' ( ) # - // We only pass the non-alphanumeric ones since alphanumerics are never encoded const QByteArray safeChars = ";,?:@&=+$-_.!~*'()#"; for (const QString &component : components) { @@ -29,9 +43,7 @@ QString normalizePathForWeb(const QString &path) { continue; } - // Lowercase and URL-encode (percent-encode) the component - QString lowered = component.toLower(); - QString percentEncoded = QUrl::toPercentEncoding(lowered, safeChars); + QString percentEncoded = QUrl::toPercentEncoding(component, safeChars); encoded.append(percentEncoded); } return encoded.join('/'); @@ -88,9 +100,9 @@ QString WebCache::getCachedPath(const QString &relativePath) const return QString(); } - // Use normalized (lowercase) path for cache lookup - QString normalizedPath = normalizePathForWeb(relativePath); - QString localPath = cacheDir() + subdir + normalizedPath; + // Use lowercase path for cache lookup (no percent-encoding in local paths) + QString lowerPath = lowercasePath(relativePath); + QString localPath = cacheDir() + subdir + lowerPath; if (!file_exists(localPath)) { @@ -165,22 +177,23 @@ void WebCache::resolveOrDownload(const QString &relativePath, const QStringList { QString fullPath = relativePath + suffix; - // Normalize path like webAO: lowercase and URL-encode - QString normalizedPath = normalizePathForWeb(fullPath); + // Lowercase path for local storage and tracking (no percent-encoding) + QString lowerPath = lowercasePath(fullPath); // Check if already downloading this path - if (m_pending_downloads.contains(normalizedPath)) + if (m_pending_downloads.contains(lowerPath)) { return; } // Check if this path previously failed (don't retry within this session) - if (m_failed_downloads.contains(normalizedPath)) + if (m_failed_downloads.contains(lowerPath)) { continue; // Try next suffix } - QString localPath = cacheDir() + subdir + normalizedPath; + // Local path uses lowercase without percent-encoding + QString localPath = cacheDir() + subdir + lowerPath; // Skip if already cached and not expired if (file_exists(localPath) && !isExpired(localPath)) @@ -188,12 +201,12 @@ void WebCache::resolveOrDownload(const QString &relativePath, const QStringList return; // Already have a valid cached file } - // Construct remote URL with normalized path - QString remoteUrl = assetUrl + normalizedPath; + // Remote URL uses percent-encoded lowercase path + QString remoteUrl = assetUrl + urlEncodePath(lowerPath); // Mark as pending and start download - m_pending_downloads.insert(normalizedPath, true); - startDownload(remoteUrl, localPath, normalizedPath); + m_pending_downloads.insert(lowerPath, true); + startDownload(remoteUrl, localPath, lowerPath); return; // Only try one suffix at a time } } From ec57822a7e18a46982004cd7ac55907e23e3002b Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sun, 1 Feb 2026 20:08:22 +0100 Subject: [PATCH 09/11] Fix getCachedPath to try file suffixes like resolveOrDownload - getCachedPath now accepts suffixes parameter to try multiple extensions - Pass suffixes from get_real_path to getCachedPath for consistent lookup - Fixes issue where cached files with extensions weren't found Co-Authored-By: Claude Opus 4.5 --- src/path_functions.cpp | 2 +- src/webcache.cpp | 31 +++++++++++++++++++------------ src/webcache.h | 3 ++- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/path_functions.cpp b/src/path_functions.cpp index 56a1c2324..c0b687bce 100644 --- a/src/path_functions.cpp +++ b/src/path_functions.cpp @@ -425,7 +425,7 @@ QString AOApplication::get_real_path(const VPath &vpath, const QStringList &suff // Check webcache if local file not found if (Options::getInstance().webcacheEnabled() && !m_serverdata.get_asset_url().isEmpty()) { - QString cached = m_webcache->getCachedPath(vpath.toQString()); + QString cached = m_webcache->getCachedPath(vpath.toQString(), suffixes); if (!cached.isEmpty()) { asset_lookup_cache.insert(qHash(vpath), cached); diff --git a/src/webcache.cpp b/src/webcache.cpp index d7ae9e5b0..1f7e72fc8 100644 --- a/src/webcache.cpp +++ b/src/webcache.cpp @@ -87,7 +87,7 @@ QString WebCache::cacheSubdir() const return subdir; } -QString WebCache::getCachedPath(const QString &relativePath) const +QString WebCache::getCachedPath(const QString &relativePath, const QStringList &suffixes) const { if (relativePath.isEmpty()) { @@ -100,21 +100,28 @@ QString WebCache::getCachedPath(const QString &relativePath) const return QString(); } - // Use lowercase path for cache lookup (no percent-encoding in local paths) - QString lowerPath = lowercasePath(relativePath); - QString localPath = cacheDir() + subdir + lowerPath; - - if (!file_exists(localPath)) + // Try each suffix + QStringList effectiveSuffixes = suffixes.isEmpty() ? QStringList{""} : suffixes; + for (const QString &suffix : effectiveSuffixes) { - return QString(); - } + // Use lowercase path for cache lookup (no percent-encoding in local paths) + QString lowerPath = lowercasePath(relativePath + suffix); + QString localPath = cacheDir() + subdir + lowerPath; - if (isExpired(localPath)) - { - return QString(); + if (!file_exists(localPath)) + { + continue; + } + + if (isExpired(localPath)) + { + continue; + } + + return localPath; } - return localPath; + return QString(); } bool WebCache::isExpired(const QString &localPath) const diff --git a/src/webcache.h b/src/webcache.h index 1cdad784c..39786175f 100644 --- a/src/webcache.h +++ b/src/webcache.h @@ -23,9 +23,10 @@ class WebCache : public QObject /** * @brief Returns the cached file path if it exists and is not expired. * @param relativePath The virtual path relative to the base (e.g., "sounds/music/song.mp3") + * @param suffixes List of file extensions to try (e.g., {".png", ".webp"}) * @return The absolute path to the cached file, or empty string if not cached/expired. */ - QString getCachedPath(const QString &relativePath) const; + QString getCachedPath(const QString &relativePath, const QStringList &suffixes = {""}) const; /** * @brief Check cache and initiate async download if file is missing or expired. From dea8e28d6123550fec11c700f7c15cc909b10cbe Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sun, 1 Feb 2026 20:24:03 +0100 Subject: [PATCH 10/11] Add signal to refresh UI when webcache downloads complete - WebCache emits fileDownloaded(relativePath) signal on successful download - Courtroom connects to signal and refreshes character icons when downloaded - AOCharButton stores character name and exposes refreshIcon() method - Character selector buttons and tree widget icons update automatically Co-Authored-By: Claude Opus 4.5 --- src/aocharbutton.cpp | 15 +++++++++++++-- src/aocharbutton.h | 5 +++++ src/courtroom.cpp | 38 ++++++++++++++++++++++++++++++++++++++ src/courtroom.h | 2 ++ src/webcache.cpp | 4 ++++ src/webcache.h | 8 ++++++++ 6 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/aocharbutton.cpp b/src/aocharbutton.cpp index d446cca3f..41bae3830 100644 --- a/src/aocharbutton.cpp +++ b/src/aocharbutton.cpp @@ -39,7 +39,18 @@ void AOCharButton::setTaken(bool enabled) void AOCharButton::setCharacter(QString character) { - QString image_path = ao_app->get_image_suffix(ao_app->get_character_path(character, "char_icon"), true); + m_character = character; + refreshIcon(); +} + +QString AOCharButton::character() const +{ + return m_character; +} + +void AOCharButton::refreshIcon() +{ + QString image_path = ao_app->get_image_suffix(ao_app->get_character_path(m_character, "char_icon"), true); setText(QString()); @@ -55,7 +66,7 @@ void AOCharButton::setCharacter(QString character) setStyleSheet("QPushButton { border-image: url(); }" "QToolTip { background-image: url(); color: #000000; " "background-color: #ffffff; border: 0px; }"); - setText(character); + setText(m_character); } } diff --git a/src/aocharbutton.h b/src/aocharbutton.h index ba6189764..a296bd659 100644 --- a/src/aocharbutton.h +++ b/src/aocharbutton.h @@ -18,6 +18,10 @@ class AOCharButton : public QPushButton void setCharacter(QString character); + QString character() const; + + void refreshIcon(); + void setTaken(bool enabled); protected: @@ -30,6 +34,7 @@ class AOCharButton : public QPushButton private: AOApplication *ao_app; + QString m_character; bool m_taken = false; AOImage *ui_taken; AOImage *ui_selector; diff --git a/src/courtroom.cpp b/src/courtroom.cpp index 7646794da..0f7edf42b 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -3,6 +3,7 @@ #include "datatypes.h" #include "moderation_functions.h" #include "options.h" +#include "webcache.h" #include @@ -137,6 +138,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) ui_debug_log->hide(); ui_debug_log->setObjectName("ui_debug_log"); connect(ao_app, &AOApplication::qt_log_message, this, &Courtroom::debug_message_handler); + connect(ao_app->webcache(), &WebCache::fileDownloaded, this, &Courtroom::on_webcache_file_downloaded); ui_server_chatlog = new AOTextArea(this); ui_server_chatlog->setReadOnly(true); @@ -6723,3 +6725,39 @@ void Courtroom::truncate_label_text(QWidget *p_widget, QString p_identifier) } qDebug().nospace() << "Truncated label text from " << label_text_tr << " (" << label_px_width << "px) to " << truncated_label << " (" << truncated_px_width << "px)"; } + +void Courtroom::on_webcache_file_downloaded(const QString &relativePath) +{ + // Check if this is a character icon and refresh the relevant button + if (relativePath.startsWith("characters/") && relativePath.endsWith("char_icon.png")) + { + // Extract character name from path: "characters/name/char_icon.png" -> "name" + QString charPath = relativePath.mid(11); // Remove "characters/" + int slashPos = charPath.indexOf('/'); + if (slashPos > 0) + { + QString charName = charPath.left(slashPos); + + // Find and refresh the button for this character (case-insensitive match) + for (AOCharButton *button : std::as_const(ui_char_button_list)) + { + if (button->character().toLower() == charName) + { + button->refreshIcon(); + break; + } + } + + // Also update the tree widget icon + QList items = ui_char_list->findItems(charName, Qt::MatchFixedString | Qt::MatchRecursive, 0); + for (QTreeWidgetItem *item : items) + { + QString iconPath = ao_app->get_image_suffix(ao_app->get_character_path(item->text(0), "char_icon"), true); + if (!iconPath.isEmpty()) + { + item->setIcon(0, QIcon(iconPath)); + } + } + } + } +} diff --git a/src/courtroom.h b/src/courtroom.h index dc8cdf3ae..edf02ff50 100644 --- a/src/courtroom.h +++ b/src/courtroom.h @@ -826,6 +826,8 @@ private Q_SLOTS: void chat_tick(); + void on_webcache_file_downloaded(const QString &relativePath); + void on_mute_list_clicked(QModelIndex p_index); void on_pair_list_clicked(QModelIndex p_index); diff --git a/src/webcache.cpp b/src/webcache.cpp index 1f7e72fc8..353fcd92d 100644 --- a/src/webcache.cpp +++ b/src/webcache.cpp @@ -283,6 +283,10 @@ void WebCache::onDownloadFinished(QNetworkReply *reply) file.close(); qDebug() << "WebCache: Successfully cached" << localPath; + + // Notify listeners that a file has been downloaded + Q_EMIT fileDownloaded(relativePath); + reply->deleteLater(); } diff --git a/src/webcache.h b/src/webcache.h index 39786175f..29e1d0f22 100644 --- a/src/webcache.h +++ b/src/webcache.h @@ -20,6 +20,14 @@ class WebCache : public QObject explicit WebCache(AOApplication *parent); ~WebCache(); +Q_SIGNALS: + /** + * @brief Emitted when a file has been successfully downloaded and cached. + * @param relativePath The lowercase relative path of the cached file (e.g., "characters/phoenix/char_icon.png") + */ + void fileDownloaded(const QString &relativePath); + +public: /** * @brief Returns the cached file path if it exists and is not expired. * @param relativePath The virtual path relative to the base (e.g., "sounds/music/song.mp3") From 63c7a88f39c03f3bea8434b4d2bd081fb7f9dcb5 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 01:43:35 +0100 Subject: [PATCH 11/11] lazy loading --- src/animationlayer.cpp | 22 +++++++++++++++++++++- src/aomusicplayer.cpp | 11 ++++++++++- src/path_functions.cpp | 3 --- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/animationlayer.cpp b/src/animationlayer.cpp index 42d903916..4ec089ef4 100644 --- a/src/animationlayer.cpp +++ b/src/animationlayer.cpp @@ -2,6 +2,7 @@ #include "aoapplication.h" #include "options.h" +#include "webcache.h" #include #include @@ -452,12 +453,23 @@ void CharacterAnimationLayer::loadCharacterEmote(QString character, QString file } int index = -1; + int non_placeholder_count = placeholder_fallback ? path_list.size() - 2 : path_list.size(); QString file_path = ao_app->get_image_path(path_list, index); if (index != -1) { m_resolved_emote = prefixed_emote_list[index]; } + // Trigger webcache download if actual character emote not found (fell back to placeholder or not found at all) + if ((index == -1 || index >= non_placeholder_count) && Options::getInstance().webcacheEnabled()) + { + static const QStringList image_suffixes{".webp", ".apng", ".gif", ".png"}; + for (int i = 0; i < non_placeholder_count; ++i) + { + ao_app->webcache()->resolveOrDownload(path_list[i].toQString(), image_suffixes); + } + } + setFileName(file_path); setPlayOnce(play_once); setResizeMode(ao_app->get_scaling(ao_app->get_emote_property(character, fileName, "scaling"))); @@ -589,7 +601,15 @@ BackgroundAnimationLayer::BackgroundAnimationLayer(AOApplication *ao_app, QWidge void BackgroundAnimationLayer::loadAndPlayAnimation(QString fileName) { - QString file_path = ao_app->get_image_suffix(ao_app->get_background_path(fileName)); + VPath vpath = ao_app->get_background_path(fileName); + QString file_path = ao_app->get_image_suffix(vpath); + + // Trigger webcache download if file not found locally + if (file_path.isEmpty() && Options::getInstance().webcacheEnabled()) + { + ao_app->webcache()->resolveOrDownload(vpath.toQString(), {".webp", ".apng", ".gif", ".png"}); + } + #ifdef DEBUG_MOVIE if (file_path.isEmpty()) { diff --git a/src/aomusicplayer.cpp b/src/aomusicplayer.cpp index c4a562cb1..84ba4618f 100644 --- a/src/aomusicplayer.cpp +++ b/src/aomusicplayer.cpp @@ -2,6 +2,7 @@ #include "file_functions.h" #include "options.h" +#include "webcache.h" #include @@ -50,7 +51,15 @@ QString AOMusicPlayer::playStream(QString song, int streamId, bool loopEnabled, { flags |= BASS_STREAM_PRESCAN | BASS_UNICODE | BASS_ASYNCFILE; - f_path = ao_app->get_real_path(ao_app->get_music_path(song)); + VPath vpath = ao_app->get_music_path(song); + f_path = ao_app->get_real_path(vpath); + + // Trigger webcache download if file not found locally + if (f_path.isEmpty() && Options::getInstance().webcacheEnabled()) + { + ao_app->webcache()->resolveOrDownload(vpath.toQString(), {".opus", ".ogg", ".mp3", ".wav"}); + } + newstream = BASS_StreamCreateFile(FALSE, f_path.utf16(), 0, 0, flags); } diff --git a/src/path_functions.cpp b/src/path_functions.cpp index c0b687bce..9ae22d214 100644 --- a/src/path_functions.cpp +++ b/src/path_functions.cpp @@ -431,9 +431,6 @@ QString AOApplication::get_real_path(const VPath &vpath, const QStringList &suff asset_lookup_cache.insert(qHash(vpath), cached); return cached; } - - // Initiate background download for future requests - m_webcache->resolveOrDownload(vpath.toQString(), suffixes); } // Not found in mount paths; check if the file is remote