Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3958da1983 | |||
| 76c98f63dd | |||
| 7a75083cf3 |
29
README.md
29
README.md
@@ -23,7 +23,7 @@ slide [-t rotation_seconds] [-T transition_seconds] [-h/--overlay-color overlay_
|
||||
|
||||
* `image_folder`: where to search for images (.jpg files)
|
||||
* `-i imageFile,...`: comma delimited list of full paths to image files to display
|
||||
* `-c path_to_config_json`: the path to an optional slide.options.json file containing configuration parameters
|
||||
* `-c path_to_config_json`: path to a JSON config file, or a directory containing `slide.options.json`
|
||||
* `-t` how many seconds to display each picture for
|
||||
* `-r` for recursive traversal of `image_folder`
|
||||
* `-s` for shuffle instead of random image rotation
|
||||
@@ -55,7 +55,7 @@ slide [-t rotation_seconds] [-T transition_seconds] [-h/--overlay-color overlay_
|
||||
To exit the application, press escape. If you're using a touch display, touch all 4 corners at the same time.
|
||||
|
||||
## Configuration file
|
||||
Slide supports loading configuration from a JSON formatted file called `slide.options.json`. This file can be specified by the `-c` command line option, we will also attempt to read `~/.config/slide/slide.options.json` and `/etc/slide/slide.options.json` in that order. The first file to load is used and its options will override command line parameters.
|
||||
Slide supports loading configuration from a JSON formatted file called `slide.options.json`. This file can be specified by the `-c` command line option (file path or directory), and we will also attempt to read `~/.config/slide/slide.options.json` and `/etc/slide/slide.options.json` in that order. The first file to load is used and its options will override command line parameters.
|
||||
The file format is:
|
||||
```
|
||||
{
|
||||
@@ -157,15 +157,28 @@ If `immichTopic` is not set, it defaults to `<topic>/immich`.
|
||||
Immich control topic (`immichTopic`):
|
||||
* `album:<id>` or `albumIds:id1,id2` — filter to one or more album IDs
|
||||
* `person:<id>` or `personIds:id1,id2` — filter to one or more person IDs
|
||||
* `reset` / `clear` — clear album/person filters
|
||||
* `user:<id>` / `userId:<id>` / `ownerId:<id>` — show all assets owned by a user (clears album/person filters)
|
||||
* `extensions:jpg,jpeg,png` — filter by file extension (useful for RAW exclusion)
|
||||
* `reset` / `clear` — clear album/person/user filters
|
||||
* JSON payloads are also accepted, for example:
|
||||
```
|
||||
{"albumIds":["..."],"personIds":["..."],"order":"desc","size":"fullsize"}
|
||||
{"albumIds":["..."],"personIds":["..."],"order":"desc","size":"fullsize","userId":"...","extensions":["jpg","jpeg"]}
|
||||
```
|
||||
|
||||
### 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.
|
||||
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
|
||||
|
||||
In the Immich web UI, go to Settings and find **API Keys** (menu labels can vary by version), then create a new key and copy it.
|
||||
|
||||
#### Required API key permissions
|
||||
|
||||
`slide` uses Immich search plus the asset view/download endpoints, so the API key should include:
|
||||
* `asset.read` — required by Immich search endpoints (used to retrieve asset metadata).
|
||||
* `asset.view` — required for the viewAsset endpoint (thumbnail/preview/fullsize).
|
||||
* `asset.download` — required if you set `size` to `original` (download endpoint).
|
||||
|
||||
Example (single source):
|
||||
```
|
||||
@@ -178,6 +191,7 @@ Example (single source):
|
||||
"order": "desc",
|
||||
"pageSize": 200,
|
||||
"maxAssets": 1000,
|
||||
"refreshSeconds": 300,
|
||||
"cachePath": "~/.cache/slide/immich",
|
||||
"cacheMaxMB": 512,
|
||||
"includeArchived": false
|
||||
@@ -205,15 +219,20 @@ Example (scheduler entry):
|
||||
Immich settings:
|
||||
* `url`: base Immich server URL (the integration appends `/api` automatically if missing).
|
||||
* `apiKey`: Immich API key (needs `asset.view`, and `asset.download` if `size` is `original`).
|
||||
* `userId`: optional user id to retrieve all assets owned by that user via the assets endpoint.
|
||||
* `albumId` or `albumIds`: optional album filters.
|
||||
* `personId` or `personIds`: optional person filters.
|
||||
* `extensions` / `allowedExtensions`: optional list of file extensions to include (for example `["jpg","jpeg","png"]`).
|
||||
* `size`: `"fullsize"`, `"preview"`, `"thumbnail"`, or `"original"` (original uses the download endpoint).
|
||||
* `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.
|
||||
If you omit `albumId`/`albumIds`/`personIds`, Immich returns all assets visible to the API key’s user.
|
||||
If `userId` is set, album/person filters are ignored and all assets for that user are fetched.
|
||||
When `immich` is set on an entry, `path` and `imageList` are ignored.
|
||||
|
||||
## Folder Options file
|
||||
|
||||
@@ -71,6 +71,19 @@ std::vector<std::string> ParseJSONStrings(QJsonObject jsonDoc, const char *key)
|
||||
return values;
|
||||
}
|
||||
|
||||
std::vector<std::string> NormalizeExtensions(const std::vector<std::string> &values) {
|
||||
std::vector<std::string> normalized;
|
||||
for (const auto &value : values)
|
||||
{
|
||||
std::string s = value;
|
||||
for (auto &c : s)
|
||||
c = static_cast<char>(::tolower(c));
|
||||
if (!s.empty())
|
||||
normalized.push_back(s);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
ImmichConfig ParseImmichConfigObject(QJsonObject immichJson) {
|
||||
ImmichConfig config;
|
||||
|
||||
@@ -81,6 +94,13 @@ ImmichConfig ParseImmichConfigObject(QJsonObject immichJson) {
|
||||
if(!apiKey.empty())
|
||||
config.apiKey = apiKey;
|
||||
|
||||
std::string userId = ParseJSONString(immichJson, "userId");
|
||||
if(!userId.empty())
|
||||
config.userId = userId;
|
||||
std::string ownerId = ParseJSONString(immichJson, "ownerId");
|
||||
if(!ownerId.empty() && config.userId.empty())
|
||||
config.userId = ownerId;
|
||||
|
||||
std::string size = ParseJSONString(immichJson, "size");
|
||||
if(!size.empty())
|
||||
config.size = size;
|
||||
@@ -109,8 +129,16 @@ ImmichConfig ParseImmichConfigObject(QJsonObject immichJson) {
|
||||
if(personIds.size() > 0)
|
||||
config.personIds = personIds;
|
||||
|
||||
std::vector<std::string> allowedExtensions = ParseJSONStrings(immichJson, "extensions");
|
||||
if(allowedExtensions.size() == 0)
|
||||
allowedExtensions = ParseJSONStrings(immichJson, "allowedExtensions");
|
||||
if(allowedExtensions.size() > 0)
|
||||
config.allowedExtensions = NormalizeExtensions(allowedExtensions);
|
||||
|
||||
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");
|
||||
|
||||
@@ -248,28 +276,36 @@ QString getAppConfigFilePath(const std::string &configPath) {
|
||||
std::string systemConfigFolder = "/etc/slide";
|
||||
QString baseConfigFilename("slide.options.json");
|
||||
|
||||
QDir directory(userConfigFolder.c_str());
|
||||
QString jsonFile = "";
|
||||
if (!configPath.empty())
|
||||
{
|
||||
directory.setPath(configPath.c_str());
|
||||
jsonFile = directory.filePath(baseConfigFilename);
|
||||
}
|
||||
if(!directory.exists(jsonFile))
|
||||
QFileInfo configInfo(QString::fromStdString(configPath));
|
||||
if (configInfo.exists() && configInfo.isFile())
|
||||
{
|
||||
directory.setPath(userConfigFolder.c_str());
|
||||
jsonFile = directory.filePath(baseConfigFilename);
|
||||
}
|
||||
if(!directory.exists(jsonFile))
|
||||
{
|
||||
directory.setPath(systemConfigFolder.c_str());
|
||||
jsonFile = directory.filePath(baseConfigFilename);
|
||||
return configInfo.absoluteFilePath();
|
||||
}
|
||||
|
||||
QDir directory(configInfo.isDir() ? configInfo.absoluteFilePath()
|
||||
: QString::fromStdString(configPath));
|
||||
QString jsonFile = directory.filePath(baseConfigFilename);
|
||||
if (directory.exists(jsonFile))
|
||||
{
|
||||
return jsonFile;
|
||||
}
|
||||
}
|
||||
|
||||
QDir userDir(userConfigFolder.c_str());
|
||||
QString userFile = userDir.filePath(baseConfigFilename);
|
||||
if (userDir.exists(userFile))
|
||||
{
|
||||
return userFile;
|
||||
}
|
||||
|
||||
QDir systemDir(systemConfigFolder.c_str());
|
||||
QString systemFile = systemDir.filePath(baseConfigFilename);
|
||||
if (systemDir.exists(systemFile))
|
||||
{
|
||||
return systemFile;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -10,12 +10,15 @@ struct ImmichConfig {
|
||||
bool enabled = false;
|
||||
std::string url = "";
|
||||
std::string apiKey = "";
|
||||
std::string userId = "";
|
||||
std::vector<std::string> albumIds;
|
||||
std::vector<std::string> personIds;
|
||||
std::vector<std::string> allowedExtensions;
|
||||
std::string size = "fullsize";
|
||||
std::string order = "desc";
|
||||
int pageSize = 200;
|
||||
int maxAssets = 1000;
|
||||
int refreshSeconds = 300;
|
||||
std::string cachePath = "";
|
||||
int cacheMaxMB = 512;
|
||||
bool includeArchived = false;
|
||||
@@ -26,6 +29,8 @@ struct ImmichConfig {
|
||||
return false;
|
||||
if (url != b.url || apiKey != b.apiKey)
|
||||
return false;
|
||||
if (userId != b.userId)
|
||||
return false;
|
||||
if (size != b.size || order != b.order)
|
||||
return false;
|
||||
if (pageSize != b.pageSize || maxAssets != b.maxAssets)
|
||||
@@ -48,6 +53,15 @@ struct ImmichConfig {
|
||||
if (personIds[i] != b.personIds[i])
|
||||
return false;
|
||||
}
|
||||
if (allowedExtensions.size() != b.allowedExtensions.size())
|
||||
return false;
|
||||
for (size_t i = 0; i < allowedExtensions.size(); ++i)
|
||||
{
|
||||
if (allowedExtensions[i] != b.allowedExtensions[i])
|
||||
return false;
|
||||
}
|
||||
if (refreshSeconds != b.refreshSeconds)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QBuffer>
|
||||
#include <QImageReader>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
@@ -12,10 +14,25 @@
|
||||
#include <QSaveFile>
|
||||
#include <QTimer>
|
||||
#include <QUrlQuery>
|
||||
#include <QFileInfo>
|
||||
|
||||
namespace {
|
||||
const int kMetadataTimeoutMs = 15000;
|
||||
const int kAssetTimeoutMs = 30000;
|
||||
QString DetectImageExtension(const QByteArray &data)
|
||||
{
|
||||
QBuffer buffer;
|
||||
buffer.setData(data);
|
||||
if (!buffer.open(QIODevice::ReadOnly))
|
||||
return "";
|
||||
QImageReader reader(&buffer);
|
||||
if (!reader.canRead())
|
||||
return "";
|
||||
QByteArray fmt = reader.format().toLower();
|
||||
if (fmt == "jpeg")
|
||||
fmt = "jpg";
|
||||
return QString::fromUtf8(fmt);
|
||||
}
|
||||
}
|
||||
|
||||
ImmichClient::ImmichClient(const ImmichConfig &configIn)
|
||||
@@ -93,13 +110,27 @@ QByteArray ImmichClient::getBytes(const QUrl &url, QString *contentType, int tim
|
||||
|
||||
QVector<ImmichAsset> ImmichClient::fetchAssets()
|
||||
{
|
||||
QVector<ImmichAsset> assets;
|
||||
if (!config.enabled)
|
||||
{
|
||||
Log("Immich config is missing url or apiKey.");
|
||||
return assets;
|
||||
return QVector<ImmichAsset>();
|
||||
}
|
||||
|
||||
if (!config.userId.empty() && config.albumIds.empty() && config.personIds.empty())
|
||||
return fetchAssetsByUser();
|
||||
|
||||
if (!config.userId.empty() && (!config.albumIds.empty() || !config.personIds.empty()))
|
||||
{
|
||||
Log("Immich userId is set but album/person filters are also set; ignoring userId for search filters.");
|
||||
}
|
||||
|
||||
return fetchAssetsBySearch();
|
||||
}
|
||||
|
||||
QVector<ImmichAsset> ImmichClient::fetchAssetsBySearch()
|
||||
{
|
||||
QVector<ImmichAsset> assets;
|
||||
|
||||
int pageSize = config.pageSize > 0 ? config.pageSize : 200;
|
||||
int maxAssets = config.maxAssets;
|
||||
bool triedZero = false;
|
||||
@@ -110,7 +141,8 @@ QVector<ImmichAsset> ImmichClient::fetchAssets()
|
||||
Log("Immich search: size=", config.size, ", order=", config.order,
|
||||
", pageSize=", pageSize, ", maxAssets=", maxAssets,
|
||||
", albumIds=", config.albumIds.size(),
|
||||
", personIds=", config.personIds.size());
|
||||
", personIds=", config.personIds.size(),
|
||||
", allowedExtensions=", config.allowedExtensions.size());
|
||||
}
|
||||
|
||||
while (true)
|
||||
@@ -170,6 +202,11 @@ QVector<ImmichAsset> ImmichClient::fetchAssets()
|
||||
ImmichAsset asset;
|
||||
asset.id = id;
|
||||
asset.originalFileName = item["originalFileName"].toString();
|
||||
if (!extensionAllowed(asset.originalFileName))
|
||||
{
|
||||
Log("Immich skip by extension: ", asset.originalFileName.toStdString());
|
||||
continue;
|
||||
}
|
||||
assets.append(asset);
|
||||
if (maxAssets > 0 && assets.size() >= maxAssets)
|
||||
return assets;
|
||||
@@ -183,12 +220,99 @@ QVector<ImmichAsset> ImmichClient::fetchAssets()
|
||||
return assets;
|
||||
}
|
||||
|
||||
QVector<ImmichAsset> ImmichClient::fetchAssetsByUser()
|
||||
{
|
||||
QVector<ImmichAsset> assets;
|
||||
|
||||
int pageSize = config.pageSize > 0 ? config.pageSize : 200;
|
||||
int maxAssets = config.maxAssets;
|
||||
int skip = 0;
|
||||
|
||||
if (ShouldLog())
|
||||
{
|
||||
Log("Immich assets: userId=", config.userId,
|
||||
", pageSize=", pageSize,
|
||||
", maxAssets=", maxAssets,
|
||||
", includeArchived=", config.includeArchived);
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
QUrl url = apiUrl("/assets");
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("take", QString::number(pageSize));
|
||||
query.addQueryItem("skip", QString::number(skip));
|
||||
query.addQueryItem("userId", QString::fromStdString(config.userId));
|
||||
if (!config.includeArchived)
|
||||
query.addQueryItem("isArchived", "false");
|
||||
url.setQuery(query);
|
||||
|
||||
QByteArray response = getBytes(url, nullptr, kMetadataTimeoutMs);
|
||||
if (response.isEmpty())
|
||||
break;
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response);
|
||||
if (!doc.isArray())
|
||||
break;
|
||||
|
||||
QJsonArray items = doc.array();
|
||||
Log("Immich user assets skip ", skip, ": ", items.size(), " assets");
|
||||
if (items.isEmpty())
|
||||
break;
|
||||
|
||||
for (const auto &value : items)
|
||||
{
|
||||
QJsonObject item = value.toObject();
|
||||
QString id = item["id"].toString();
|
||||
if (id.isEmpty())
|
||||
continue;
|
||||
ImmichAsset asset;
|
||||
asset.id = id;
|
||||
asset.originalFileName = item["originalFileName"].toString();
|
||||
if (!extensionAllowed(asset.originalFileName))
|
||||
{
|
||||
Log("Immich skip by extension: ", asset.originalFileName.toStdString());
|
||||
continue;
|
||||
}
|
||||
assets.append(asset);
|
||||
if (maxAssets > 0 && assets.size() >= maxAssets)
|
||||
return assets;
|
||||
}
|
||||
|
||||
if (items.size() < pageSize)
|
||||
break;
|
||||
skip += pageSize;
|
||||
}
|
||||
|
||||
return assets;
|
||||
}
|
||||
|
||||
bool ImmichClient::extensionAllowed(const QString &filename) const
|
||||
{
|
||||
if (config.allowedExtensions.empty())
|
||||
return true;
|
||||
QString ext = QFileInfo(filename).suffix().toLower();
|
||||
if (ext.isEmpty())
|
||||
return false;
|
||||
for (const auto &allowed : config.allowedExtensions)
|
||||
{
|
||||
if (ext == QString::fromStdString(allowed))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
@@ -253,6 +377,11 @@ QString ImmichAssetCache::findExisting(const QString &assetId) const
|
||||
QStringList matches = dir.entryList(QStringList() << (assetId + "_*"), QDir::Files, QDir::Time);
|
||||
if (matches.isEmpty())
|
||||
return "";
|
||||
for (const auto &match : matches)
|
||||
{
|
||||
if (match.endsWith(".skip"))
|
||||
return dir.filePath(match);
|
||||
}
|
||||
return dir.filePath(matches.first());
|
||||
}
|
||||
|
||||
@@ -314,18 +443,84 @@ QString ImmichAssetCache::getCachedPath(const QString &assetId, const QString &a
|
||||
|
||||
QString existing = findExisting(assetId);
|
||||
if (!existing.isEmpty())
|
||||
{
|
||||
if (existing.endsWith(".skip"))
|
||||
{
|
||||
Log("Immich skip marker: ", assetId.toStdString());
|
||||
return "";
|
||||
}
|
||||
QFileInfo info(existing);
|
||||
if (info.size() <= 0)
|
||||
{
|
||||
QFile::remove(existing);
|
||||
}
|
||||
else if (existing.endsWith(".img"))
|
||||
{
|
||||
QImageReader reader(existing);
|
||||
if (!reader.canRead())
|
||||
{
|
||||
Log("Immich cache invalid: ", existing.toStdString());
|
||||
QFile::remove(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("Immich cache hit: ", assetId.toStdString());
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("Immich cache hit: ", assetId.toStdString());
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray data;
|
||||
QString contentType;
|
||||
if (!client.downloadAsset(assetId, data, contentType))
|
||||
return "";
|
||||
|
||||
QString detectedExt = DetectImageExtension(data);
|
||||
if (detectedExt.isEmpty())
|
||||
{
|
||||
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";
|
||||
QDir dir(cacheDirPath);
|
||||
QFile skipFile(dir.filePath(skipName));
|
||||
if (skipFile.open(QIODevice::WriteOnly))
|
||||
{
|
||||
skipFile.write("unsupported");
|
||||
skipFile.close();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
QString safeName = sanitizeFileName(assetName);
|
||||
QString extension = extensionForContentType(contentType);
|
||||
QString extension = detectedExt;
|
||||
if (extension.isEmpty())
|
||||
extension = extensionForContentType(contentType);
|
||||
if (extension == "img")
|
||||
{
|
||||
QString suffix = QFileInfo(assetName).suffix().toLower();
|
||||
if (!suffix.isEmpty())
|
||||
extension = suffix;
|
||||
}
|
||||
QString filename = assetId + "_" + safeName + "." + extension;
|
||||
|
||||
QDir dir(cacheDirPath);
|
||||
|
||||
@@ -21,8 +21,12 @@ class ImmichClient {
|
||||
explicit ImmichClient(const ImmichConfig &config);
|
||||
QVector<ImmichAsset> fetchAssets();
|
||||
bool downloadAsset(const QString &assetId, QByteArray &data, QString &contentType);
|
||||
bool downloadAssetWithSize(const QString &assetId, const QString &sizeOverride, QByteArray &data, QString &contentType);
|
||||
|
||||
private:
|
||||
QVector<ImmichAsset> fetchAssetsBySearch();
|
||||
QVector<ImmichAsset> fetchAssetsByUser();
|
||||
bool extensionAllowed(const QString &filename) const;
|
||||
QUrl apiUrl(const QString &path) const;
|
||||
QNetworkRequest makeRequest(const QUrl &url) const;
|
||||
QByteArray postJson(const QUrl &url, const QJsonObject &body, QString *contentType, int timeoutMs);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "immichpathtraverser.h"
|
||||
#include "logger.h"
|
||||
#include <QDateTime>
|
||||
|
||||
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<ImmichPathTraverser*>(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();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "immichclient.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QDateTime>
|
||||
|
||||
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<QString, QString> assetNames;
|
||||
QDateTime lastRefresh;
|
||||
};
|
||||
|
||||
#endif // IMMICHPATHTRAVERSER_H
|
||||
|
||||
91
src/main.cpp
91
src/main.cpp
@@ -292,6 +292,8 @@ static bool ApplyImmichPayload(ImmichConfig &config, const QString &payload)
|
||||
if (obj.contains("albumId") && obj["albumId"].isString())
|
||||
{
|
||||
config.albumIds = { obj["albumId"].toString().toStdString() };
|
||||
config.userId.clear();
|
||||
config.allowedExtensions.clear();
|
||||
changed = true;
|
||||
}
|
||||
if (obj.contains("albumIds") && obj["albumIds"].isArray())
|
||||
@@ -303,11 +305,15 @@ static bool ApplyImmichPayload(ImmichConfig &config, const QString &payload)
|
||||
if (value.isString())
|
||||
config.albumIds.push_back(value.toString().toStdString());
|
||||
}
|
||||
config.userId.clear();
|
||||
config.allowedExtensions.clear();
|
||||
changed = true;
|
||||
}
|
||||
if (obj.contains("personId") && obj["personId"].isString())
|
||||
{
|
||||
config.personIds = { obj["personId"].toString().toStdString() };
|
||||
config.userId.clear();
|
||||
config.allowedExtensions.clear();
|
||||
changed = true;
|
||||
}
|
||||
if (obj.contains("personIds") && obj["personIds"].isArray())
|
||||
@@ -319,6 +325,24 @@ static bool ApplyImmichPayload(ImmichConfig &config, const QString &payload)
|
||||
if (value.isString())
|
||||
config.personIds.push_back(value.toString().toStdString());
|
||||
}
|
||||
config.userId.clear();
|
||||
config.allowedExtensions.clear();
|
||||
changed = true;
|
||||
}
|
||||
if (obj.contains("userId") && obj["userId"].isString())
|
||||
{
|
||||
config.userId = obj["userId"].toString().toStdString();
|
||||
config.albumIds.clear();
|
||||
config.personIds.clear();
|
||||
config.allowedExtensions.clear();
|
||||
changed = true;
|
||||
}
|
||||
if (obj.contains("ownerId") && obj["ownerId"].isString())
|
||||
{
|
||||
config.userId = obj["ownerId"].toString().toStdString();
|
||||
config.albumIds.clear();
|
||||
config.personIds.clear();
|
||||
config.allowedExtensions.clear();
|
||||
changed = true;
|
||||
}
|
||||
if (obj.contains("order") && obj["order"].isString())
|
||||
@@ -341,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())
|
||||
@@ -364,6 +393,30 @@ static bool ApplyImmichPayload(ImmichConfig &config, const QString &payload)
|
||||
{
|
||||
config.albumIds.clear();
|
||||
config.personIds.clear();
|
||||
config.userId.clear();
|
||||
config.allowedExtensions.clear();
|
||||
changed = true;
|
||||
}
|
||||
if (obj.contains("extensions") && obj["extensions"].isArray())
|
||||
{
|
||||
config.allowedExtensions.clear();
|
||||
QJsonArray arr = obj["extensions"].toArray();
|
||||
for (const auto &value : arr)
|
||||
{
|
||||
if (value.isString())
|
||||
config.allowedExtensions.push_back(value.toString().toLower().toStdString());
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if (obj.contains("allowedExtensions") && obj["allowedExtensions"].isArray())
|
||||
{
|
||||
config.allowedExtensions.clear();
|
||||
QJsonArray arr = obj["allowedExtensions"].toArray();
|
||||
for (const auto &value : arr)
|
||||
{
|
||||
if (value.isString())
|
||||
config.allowedExtensions.push_back(value.toString().toLower().toStdString());
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
@@ -391,26 +444,54 @@ static bool ApplyImmichPayload(ImmichConfig &config, const QString &payload)
|
||||
{
|
||||
config.albumIds.clear();
|
||||
config.personIds.clear();
|
||||
config.userId.clear();
|
||||
config.allowedExtensions.clear();
|
||||
return true;
|
||||
}
|
||||
if (key == "album" || key == "albumid")
|
||||
{
|
||||
config.albumIds = { value.toStdString() };
|
||||
config.userId.clear();
|
||||
config.allowedExtensions.clear();
|
||||
return true;
|
||||
}
|
||||
if (key == "albums" || key == "albumids")
|
||||
{
|
||||
config.albumIds = SplitCsv(value);
|
||||
config.userId.clear();
|
||||
config.allowedExtensions.clear();
|
||||
return true;
|
||||
}
|
||||
if (key == "person" || key == "personid")
|
||||
{
|
||||
config.personIds = { value.toStdString() };
|
||||
config.userId.clear();
|
||||
config.allowedExtensions.clear();
|
||||
return true;
|
||||
}
|
||||
if (key == "persons" || key == "personids")
|
||||
{
|
||||
config.personIds = SplitCsv(value);
|
||||
config.userId.clear();
|
||||
config.allowedExtensions.clear();
|
||||
return true;
|
||||
}
|
||||
if (key == "user" || key == "userid" || key == "ownerid")
|
||||
{
|
||||
config.userId = value.toStdString();
|
||||
config.albumIds.clear();
|
||||
config.personIds.clear();
|
||||
config.allowedExtensions.clear();
|
||||
return true;
|
||||
}
|
||||
if (key == "extensions" || key == "allowedextensions")
|
||||
{
|
||||
config.allowedExtensions = SplitCsv(value);
|
||||
for (auto &ext : config.allowedExtensions)
|
||||
{
|
||||
for (auto &c : ext)
|
||||
c = static_cast<char>(::tolower(c));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (key == "order")
|
||||
@@ -452,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;
|
||||
}
|
||||
|
||||
@@ -181,6 +181,16 @@ void MainWindow::updateImage()
|
||||
|
||||
QPixmap p;
|
||||
p.load( currentImage.filename.c_str() );
|
||||
if (p.isNull())
|
||||
{
|
||||
Log("Error: failed to load image: ", currentImage.filename);
|
||||
warn("Failed to load image.");
|
||||
if (switcher != nullptr)
|
||||
{
|
||||
switcher->scheduleImageUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Log("size:", p.width(), "x", p.height(), "(window:", width(), ",", height(), ")");
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user