1 Commits

Author SHA1 Message Date
3958da1983 improve handling of images from immich albums
Some checks failed
continuous-integration/drone/push Build was killed
continuous-integration/drone/tag Build is passing
2026-02-02 09:02:06 +11:00
10 changed files with 81 additions and 11 deletions

View File

@@ -167,7 +167,7 @@ Immich control topic (`immichTopic`):
### Immich configuration (lightweight + low power) ### 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 #### Getting an Immich API key
@@ -191,6 +191,7 @@ Example (single source):
"order": "desc", "order": "desc",
"pageSize": 200, "pageSize": 200,
"maxAssets": 1000, "maxAssets": 1000,
"refreshSeconds": 300,
"cachePath": "~/.cache/slide/immich", "cachePath": "~/.cache/slide/immich",
"cacheMaxMB": 512, "cacheMaxMB": 512,
"includeArchived": false "includeArchived": false
@@ -226,6 +227,7 @@ Immich settings:
* `order`: `"asc"` or `"desc"` ordering for asset search. * `order`: `"asc"` or `"desc"` ordering for asset search.
* `pageSize`: assets fetched per page. * `pageSize`: assets fetched per page.
* `maxAssets`: cap on total assets fetched (0 means no cap). * `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. * `cachePath`: local cache directory for downloaded images.
* `cacheMaxMB`: maximum cache size in MB (0 disables cleanup). * `cacheMaxMB`: maximum cache size in MB (0 disables cleanup).
* `includeArchived`: include archived assets in search results. * `includeArchived`: include archived assets in search results.

View File

@@ -137,6 +137,8 @@ ImmichConfig ParseImmichConfigObject(QJsonObject immichJson) {
SetJSONInt(config.pageSize, immichJson, "pageSize"); SetJSONInt(config.pageSize, immichJson, "pageSize");
SetJSONInt(config.maxAssets, immichJson, "maxAssets"); SetJSONInt(config.maxAssets, immichJson, "maxAssets");
SetJSONInt(config.refreshSeconds, immichJson, "refreshSeconds");
SetJSONInt(config.refreshSeconds, immichJson, "refreshIntervalSeconds");
SetJSONInt(config.cacheMaxMB, immichJson, "cacheMaxMB"); SetJSONInt(config.cacheMaxMB, immichJson, "cacheMaxMB");
SetJSONBool(config.includeArchived, immichJson, "includeArchived"); SetJSONBool(config.includeArchived, immichJson, "includeArchived");

View File

@@ -18,6 +18,7 @@ struct ImmichConfig {
std::string order = "desc"; std::string order = "desc";
int pageSize = 200; int pageSize = 200;
int maxAssets = 1000; int maxAssets = 1000;
int refreshSeconds = 300;
std::string cachePath = ""; std::string cachePath = "";
int cacheMaxMB = 512; int cacheMaxMB = 512;
bool includeArchived = false; bool includeArchived = false;
@@ -59,6 +60,8 @@ struct ImmichConfig {
if (allowedExtensions[i] != b.allowedExtensions[i]) if (allowedExtensions[i] != b.allowedExtensions[i])
return false; return false;
} }
if (refreshSeconds != b.refreshSeconds)
return false;
return true; return true;
} }

View File

@@ -242,7 +242,7 @@ const ImageDetails ShuffleImageSelector::getNextImage(const ImageDisplayOptions
void ShuffleImageSelector::reloadImagesIfNoneLeft() 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; current_image_shuffle = 0;
images = pathTraverser->getImages(); images = pathTraverser->getImages();
@@ -305,7 +305,7 @@ const ImageDetails SortedImageSelector::getNextImage(const ImageDisplayOptions &
void SortedImageSelector::reloadImagesIfEmpty() void SortedImageSelector::reloadImagesIfEmpty()
{ {
if (images.size() == 0) if (images.size() == 0 || pathTraverser->shouldReloadImages())
{ {
images = pathTraverser->getImages(); images = pathTraverser->getImages();
std::sort(images.begin(), images.end()); std::sort(images.begin(), images.end());

View File

@@ -303,11 +303,16 @@ bool ImmichClient::extensionAllowed(const QString &filename) const
} }
bool ImmichClient::downloadAsset(const QString &assetId, QByteArray &data, QString &contentType) 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) if (!config.enabled)
return false; return false;
QString size = QString::fromStdString(config.size).trimmed().toLower(); QString size = sizeOverride.trimmed().toLower();
if (size.isEmpty()) if (size.isEmpty())
size = "fullsize"; size = "fullsize";
@@ -479,6 +484,21 @@ QString ImmichAssetCache::getCachedPath(const QString &assetId, const QString &a
if (detectedExt.isEmpty()) if (detectedExt.isEmpty())
{ {
Log("Immich download not an image for asset: ", assetId.toStdString()); Log("Immich download not an image for asset: ", assetId.toStdString());
QByteArray fallbackData;
QString fallbackType;
if (client.downloadAssetWithSize(assetId, "preview", fallbackData, fallbackType))
{
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"; QString skipName = assetId + "_unsupported.skip";
QDir dir(cacheDirPath); QDir dir(cacheDirPath);
QFile skipFile(dir.filePath(skipName)); QFile skipFile(dir.filePath(skipName));
@@ -489,6 +509,7 @@ QString ImmichAssetCache::getCachedPath(const QString &assetId, const QString &a
} }
return ""; return "";
} }
}
QString safeName = sanitizeFileName(assetName); QString safeName = sanitizeFileName(assetName);
QString extension = detectedExt; QString extension = detectedExt;

View File

@@ -21,6 +21,7 @@ class ImmichClient {
explicit ImmichClient(const ImmichConfig &config); explicit ImmichClient(const ImmichConfig &config);
QVector<ImmichAsset> fetchAssets(); QVector<ImmichAsset> fetchAssets();
bool downloadAsset(const QString &assetId, QByteArray &data, QString &contentType); bool downloadAsset(const QString &assetId, QByteArray &data, QString &contentType);
bool downloadAssetWithSize(const QString &assetId, const QString &sizeOverride, QByteArray &data, QString &contentType);
private: private:
QVector<ImmichAsset> fetchAssetsBySearch(); QVector<ImmichAsset> fetchAssetsBySearch();

View File

@@ -1,5 +1,6 @@
#include "immichpathtraverser.h" #include "immichpathtraverser.h"
#include "logger.h" #include "logger.h"
#include <QDateTime>
ImmichPathTraverser::ImmichPathTraverser(const ImmichConfig &configIn) ImmichPathTraverser::ImmichPathTraverser(const ImmichConfig &configIn)
: PathTraverser(""), : PathTraverser(""),
@@ -25,10 +26,16 @@ void ImmichPathTraverser::loadAssets()
assetNames.insert(asset.id, asset.originalFileName); assetNames.insert(asset.id, asset.originalFileName);
} }
Log("Immich assets loaded: ", assetIds.size()); Log("Immich assets loaded: ", assetIds.size());
lastRefresh = QDateTime::currentDateTime();
} }
QStringList ImmichPathTraverser::getImages() const QStringList ImmichPathTraverser::getImages() const
{ {
if (refreshDue())
{
Log("Immich refresh due, reloading assets.");
const_cast<ImmichPathTraverser*>(this)->loadAssets();
}
return assetIds; return assetIds;
} }
@@ -45,3 +52,17 @@ ImageDisplayOptions ImmichPathTraverser::UpdateOptionsForImage(const std::string
Q_UNUSED(filename); Q_UNUSED(filename);
return options; 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();
}

View File

@@ -5,6 +5,7 @@
#include "immichclient.h" #include "immichclient.h"
#include <QHash> #include <QHash>
#include <QDateTime>
class ImmichPathTraverser : public PathTraverser class ImmichPathTraverser : public PathTraverser
{ {
@@ -14,15 +15,18 @@ class ImmichPathTraverser : public PathTraverser
QStringList getImages() const override; QStringList getImages() const override;
virtual const std::string getImagePath(const std::string image) 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; virtual ImageDisplayOptions UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& options) const override;
bool shouldReloadImages() const override;
private: private:
void loadAssets(); void loadAssets();
bool refreshDue() const;
ImmichConfig config; ImmichConfig config;
mutable ImmichClient client; mutable ImmichClient client;
mutable ImmichAssetCache cache; mutable ImmichAssetCache cache;
QStringList assetIds; QStringList assetIds;
QHash<QString, QString> assetNames; QHash<QString, QString> assetNames;
QDateTime lastRefresh;
}; };
#endif // IMMICHPATHTRAVERSER_H #endif // IMMICHPATHTRAVERSER_H

View File

@@ -365,6 +365,11 @@ static bool ApplyImmichPayload(ImmichConfig &config, const QString &payload)
config.maxAssets = (int)obj["maxAssets"].toDouble(); config.maxAssets = (int)obj["maxAssets"].toDouble();
changed = true; changed = true;
} }
if (obj.contains("refreshSeconds") && obj["refreshSeconds"].isDouble())
{
config.refreshSeconds = (int)obj["refreshSeconds"].toDouble();
changed = true;
}
if (obj.contains("includeArchived")) if (obj.contains("includeArchived"))
{ {
if (obj["includeArchived"].isBool()) if (obj["includeArchived"].isBool())
@@ -528,6 +533,16 @@ static bool ApplyImmichPayload(ImmichConfig &config, const QString &payload)
return true; return true;
} }
} }
if (key == "refreshseconds")
{
bool ok = false;
int parsed = value.toInt(&ok);
if (ok)
{
config.refreshSeconds = parsed;
return true;
}
}
return false; return false;
} }

View File

@@ -17,6 +17,7 @@ class PathTraverser
virtual QStringList getImages() const = 0; virtual QStringList getImages() const = 0;
virtual const std::string getImagePath(const std::string image) 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 ImageDisplayOptions UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& baseOptions) const = 0;
virtual bool shouldReloadImages() const { return false; }
protected: protected:
const std::string path; const std::string path;