diff --git a/README.md b/README.md index fe91a01..370e9e1 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ Immich control topic (`immichTopic`): ### Immich configuration (lightweight + low power) -Immich uses an API key and a `/api` base path. This integration requests the asset search endpoint and downloads the configured image size into a local cache before displaying them. That keeps bandwidth and power usage low while still letting `slide` do its normal scaling and transitions. If you have RAW images, set `size` to `preview`/`thumbnail` or use `extensions` to limit results to JPEG/PNG. +Immich uses an API key and a `/api` base path. This integration requests the asset search endpoint and downloads the configured image size into a local cache before displaying them. That keeps bandwidth and power usage low while still letting `slide` do its normal scaling and transitions. If you have RAW/HEIC images, set `size` to `preview`/`thumbnail` or use `extensions` to limit results to JPEG/PNG; `slide` will also fall back to `preview` if a `fullsize` download isn't readable. #### Getting an Immich API key @@ -191,6 +191,7 @@ Example (single source): "order": "desc", "pageSize": 200, "maxAssets": 1000, + "refreshSeconds": 300, "cachePath": "~/.cache/slide/immich", "cacheMaxMB": 512, "includeArchived": false @@ -226,6 +227,7 @@ Immich settings: * `order`: `"asc"` or `"desc"` ordering for asset search. * `pageSize`: assets fetched per page. * `maxAssets`: cap on total assets fetched (0 means no cap). +* `refreshSeconds`: refresh interval for reloading Immich assets (0 disables). * `cachePath`: local cache directory for downloaded images. * `cacheMaxMB`: maximum cache size in MB (0 disables cleanup). * `includeArchived`: include archived assets in search results. diff --git a/src/appconfig.cpp b/src/appconfig.cpp index 394000a..a9b99a9 100644 --- a/src/appconfig.cpp +++ b/src/appconfig.cpp @@ -137,6 +137,8 @@ ImmichConfig ParseImmichConfigObject(QJsonObject immichJson) { SetJSONInt(config.pageSize, immichJson, "pageSize"); SetJSONInt(config.maxAssets, immichJson, "maxAssets"); + SetJSONInt(config.refreshSeconds, immichJson, "refreshSeconds"); + SetJSONInt(config.refreshSeconds, immichJson, "refreshIntervalSeconds"); SetJSONInt(config.cacheMaxMB, immichJson, "cacheMaxMB"); SetJSONBool(config.includeArchived, immichJson, "includeArchived"); diff --git a/src/appconfig.h b/src/appconfig.h index 4e557b0..c25930e 100644 --- a/src/appconfig.h +++ b/src/appconfig.h @@ -18,6 +18,7 @@ struct ImmichConfig { std::string order = "desc"; int pageSize = 200; int maxAssets = 1000; + int refreshSeconds = 300; std::string cachePath = ""; int cacheMaxMB = 512; bool includeArchived = false; @@ -59,6 +60,8 @@ struct ImmichConfig { if (allowedExtensions[i] != b.allowedExtensions[i]) return false; } + if (refreshSeconds != b.refreshSeconds) + return false; return true; } diff --git a/src/imageselector.cpp b/src/imageselector.cpp index a47329c..1991d0d 100644 --- a/src/imageselector.cpp +++ b/src/imageselector.cpp @@ -242,7 +242,7 @@ const ImageDetails ShuffleImageSelector::getNextImage(const ImageDisplayOptions void ShuffleImageSelector::reloadImagesIfNoneLeft() { - if (images.size() == 0 || current_image_shuffle >= images.size()) + if (images.size() == 0 || current_image_shuffle >= images.size() || pathTraverser->shouldReloadImages()) { current_image_shuffle = 0; images = pathTraverser->getImages(); @@ -305,7 +305,7 @@ const ImageDetails SortedImageSelector::getNextImage(const ImageDisplayOptions & void SortedImageSelector::reloadImagesIfEmpty() { - if (images.size() == 0) + if (images.size() == 0 || pathTraverser->shouldReloadImages()) { images = pathTraverser->getImages(); std::sort(images.begin(), images.end()); diff --git a/src/immichclient.cpp b/src/immichclient.cpp index 6974c1d..8c45d50 100644 --- a/src/immichclient.cpp +++ b/src/immichclient.cpp @@ -303,11 +303,16 @@ bool ImmichClient::extensionAllowed(const QString &filename) const } bool ImmichClient::downloadAsset(const QString &assetId, QByteArray &data, QString &contentType) +{ + return downloadAssetWithSize(assetId, QString::fromStdString(config.size), data, contentType); +} + +bool ImmichClient::downloadAssetWithSize(const QString &assetId, const QString &sizeOverride, QByteArray &data, QString &contentType) { if (!config.enabled) return false; - QString size = QString::fromStdString(config.size).trimmed().toLower(); + QString size = sizeOverride.trimmed().toLower(); if (size.isEmpty()) size = "fullsize"; @@ -479,15 +484,31 @@ QString ImmichAssetCache::getCachedPath(const QString &assetId, const QString &a if (detectedExt.isEmpty()) { Log("Immich download not an image for asset: ", assetId.toStdString()); - QString skipName = assetId + "_unsupported.skip"; - QDir dir(cacheDirPath); - QFile skipFile(dir.filePath(skipName)); - if (skipFile.open(QIODevice::WriteOnly)) + QByteArray fallbackData; + QString fallbackType; + if (client.downloadAssetWithSize(assetId, "preview", fallbackData, fallbackType)) { - skipFile.write("unsupported"); - skipFile.close(); + QString fallbackExt = DetectImageExtension(fallbackData); + if (!fallbackExt.isEmpty()) + { + Log("Immich fallback to preview succeeded for asset: ", assetId.toStdString()); + data = fallbackData; + contentType = fallbackType; + detectedExt = fallbackExt; + } + } + if (detectedExt.isEmpty()) + { + QString skipName = assetId + "_unsupported.skip"; + QDir dir(cacheDirPath); + QFile skipFile(dir.filePath(skipName)); + if (skipFile.open(QIODevice::WriteOnly)) + { + skipFile.write("unsupported"); + skipFile.close(); + } + return ""; } - return ""; } QString safeName = sanitizeFileName(assetName); diff --git a/src/immichclient.h b/src/immichclient.h index af71040..efa9eb7 100644 --- a/src/immichclient.h +++ b/src/immichclient.h @@ -21,6 +21,7 @@ class ImmichClient { explicit ImmichClient(const ImmichConfig &config); QVector fetchAssets(); bool downloadAsset(const QString &assetId, QByteArray &data, QString &contentType); + bool downloadAssetWithSize(const QString &assetId, const QString &sizeOverride, QByteArray &data, QString &contentType); private: QVector fetchAssetsBySearch(); diff --git a/src/immichpathtraverser.cpp b/src/immichpathtraverser.cpp index a3232ec..d6e1546 100644 --- a/src/immichpathtraverser.cpp +++ b/src/immichpathtraverser.cpp @@ -1,5 +1,6 @@ #include "immichpathtraverser.h" #include "logger.h" +#include ImmichPathTraverser::ImmichPathTraverser(const ImmichConfig &configIn) : PathTraverser(""), @@ -25,10 +26,16 @@ void ImmichPathTraverser::loadAssets() assetNames.insert(asset.id, asset.originalFileName); } Log("Immich assets loaded: ", assetIds.size()); + lastRefresh = QDateTime::currentDateTime(); } QStringList ImmichPathTraverser::getImages() const { + if (refreshDue()) + { + Log("Immich refresh due, reloading assets."); + const_cast(this)->loadAssets(); + } return assetIds; } @@ -45,3 +52,17 @@ ImageDisplayOptions ImmichPathTraverser::UpdateOptionsForImage(const std::string Q_UNUSED(filename); return options; } + +bool ImmichPathTraverser::refreshDue() const +{ + if (config.refreshSeconds <= 0) + return false; + if (!lastRefresh.isValid()) + return true; + return lastRefresh.secsTo(QDateTime::currentDateTime()) >= config.refreshSeconds; +} + +bool ImmichPathTraverser::shouldReloadImages() const +{ + return refreshDue(); +} diff --git a/src/immichpathtraverser.h b/src/immichpathtraverser.h index 8469190..5b7e219 100644 --- a/src/immichpathtraverser.h +++ b/src/immichpathtraverser.h @@ -5,6 +5,7 @@ #include "immichclient.h" #include +#include class ImmichPathTraverser : public PathTraverser { @@ -14,15 +15,18 @@ class ImmichPathTraverser : public PathTraverser QStringList getImages() const override; virtual const std::string getImagePath(const std::string image) const override; virtual ImageDisplayOptions UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& options) const override; + bool shouldReloadImages() const override; private: void loadAssets(); + bool refreshDue() const; ImmichConfig config; mutable ImmichClient client; mutable ImmichAssetCache cache; QStringList assetIds; QHash assetNames; + QDateTime lastRefresh; }; #endif // IMMICHPATHTRAVERSER_H diff --git a/src/main.cpp b/src/main.cpp index 545d9a3..f453530 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -365,6 +365,11 @@ static bool ApplyImmichPayload(ImmichConfig &config, const QString &payload) config.maxAssets = (int)obj["maxAssets"].toDouble(); changed = true; } + if (obj.contains("refreshSeconds") && obj["refreshSeconds"].isDouble()) + { + config.refreshSeconds = (int)obj["refreshSeconds"].toDouble(); + changed = true; + } if (obj.contains("includeArchived")) { if (obj["includeArchived"].isBool()) @@ -528,6 +533,16 @@ static bool ApplyImmichPayload(ImmichConfig &config, const QString &payload) return true; } } + if (key == "refreshseconds") + { + bool ok = false; + int parsed = value.toInt(&ok); + if (ok) + { + config.refreshSeconds = parsed; + return true; + } + } return false; } diff --git a/src/pathtraverser.h b/src/pathtraverser.h index 412183a..59b1d86 100644 --- a/src/pathtraverser.h +++ b/src/pathtraverser.h @@ -17,6 +17,7 @@ class PathTraverser virtual QStringList getImages() const = 0; virtual const std::string getImagePath(const std::string image) const = 0; virtual ImageDisplayOptions UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& baseOptions) const = 0; + virtual bool shouldReloadImages() const { return false; } protected: const std::string path;