improve handling of images from immich albums
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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,15 +484,31 @@ 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());
|
||||||
QString skipName = assetId + "_unsupported.skip";
|
QByteArray fallbackData;
|
||||||
QDir dir(cacheDirPath);
|
QString fallbackType;
|
||||||
QFile skipFile(dir.filePath(skipName));
|
if (client.downloadAssetWithSize(assetId, "preview", fallbackData, fallbackType))
|
||||||
if (skipFile.open(QIODevice::WriteOnly))
|
|
||||||
{
|
{
|
||||||
skipFile.write("unsupported");
|
QString fallbackExt = DetectImageExtension(fallbackData);
|
||||||
skipFile.close();
|
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);
|
QString safeName = sanitizeFileName(assetName);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
15
src/main.cpp
15
src/main.cpp
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user