first commit
This commit is contained in:
53
README.md
53
README.md
@@ -117,12 +117,65 @@ Supported keys and values in the JSON configuration are:
|
||||
* `opacity` : the same as the command line `-o` argument
|
||||
* `blur` : the same as the command line `-b` argument
|
||||
* `debug` : set to true to enable verbose output from the program
|
||||
* `immich` : connect to an Immich server instead of a local path (see below)
|
||||
* `scheduler` : this entry is an array of possible path values and associated settings. This key lets you manage display times/settings for a collection of paths. In the example above the top entry shows ONLY files from a Redit feed between 2 and 4pm, ONLY files from the `show_peak_times` folder from 8am to 10am and then 4pm to 7pm. At all other times it alternates displaying files in the `always_show_1` and `always_show_2` folder.
|
||||
* `exclusive` : When set to `true` only this entry will be used when it is in its valid time window.
|
||||
* `times` : times is a JSON array of start and end times in which it is valid to display this image. The time is in the format HH:MM:SS and is based on the systems local time. If `start` isn't defined then it defaults to the start of the day, if `end` isn't defined it defaults to the end of the day.
|
||||
* `path` : the path to image files
|
||||
* `stretch` : as above
|
||||
|
||||
### Immich configuration (lightweight + low power)
|
||||
|
||||
Immich uses an API key and a `/api` base path. This integration requests the asset search endpoint and downloads thumbnail images into a local cache before displaying them. That keeps bandwidth and power usage low while still letting `slide` do its normal scaling and transitions.
|
||||
|
||||
Example (single source):
|
||||
```
|
||||
{
|
||||
"immich": {
|
||||
"url": "http://immich.local:2283",
|
||||
"apiKey": "IMMICH_API_KEY",
|
||||
"albumId": "b7f3c8b2-2e3f-4b32-9dc9-8c3f8b0a3ef7",
|
||||
"size": "fullsize",
|
||||
"order": "desc",
|
||||
"pageSize": 200,
|
||||
"maxAssets": 1000,
|
||||
"cachePath": "~/.cache/slide/immich",
|
||||
"cacheMaxMB": 512,
|
||||
"includeArchived": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example (scheduler entry):
|
||||
```
|
||||
{
|
||||
"scheduler": [
|
||||
{
|
||||
"exclusive": true,
|
||||
"immich": {
|
||||
"url": "http://immich.local:2283",
|
||||
"apiKey": "IMMICH_API_KEY",
|
||||
"albumIds": ["b7f3c8b2-2e3f-4b32-9dc9-8c3f8b0a3ef7"],
|
||||
"size": "fullsize"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
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`).
|
||||
* `albumId` or `albumIds`: optional album filters.
|
||||
* `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).
|
||||
* `cachePath`: local cache directory for downloaded thumbnails.
|
||||
* `cacheMaxMB`: maximum cache size in MB (0 disables cleanup).
|
||||
* `includeArchived`: include archived assets in search results.
|
||||
When `immich` is set on an entry, `path` and `imageList` are ignored.
|
||||
|
||||
## Folder Options file
|
||||
When using the default or recursive folder mode we support having per folder display options. The options are stored in a file called "options.json" in the images folder and support a subset of the applications configuration settings:
|
||||
```
|
||||
|
||||
35
sbin/build_deb.sh
Normal file
35
sbin/build_deb.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
VERSION="${1:-${VERSION:-0.0.0}}"
|
||||
VERSION="${VERSION#v}"
|
||||
ARCH="${ARCH:-$(dpkg --print-architecture)}"
|
||||
|
||||
PACKAGE_NAME="slide"
|
||||
BUILD_DIR="$ROOT_DIR/build"
|
||||
DIST_DIR="$ROOT_DIR/dist"
|
||||
STAGE_DIR="$BUILD_DIR/deb"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
make build
|
||||
|
||||
rm -rf "$STAGE_DIR"
|
||||
mkdir -p "$STAGE_DIR/DEBIAN" "$STAGE_DIR/usr/local/bin" "$DIST_DIR"
|
||||
|
||||
install -m 0755 "$BUILD_DIR/slide" "$STAGE_DIR/usr/local/bin/slide"
|
||||
|
||||
cat > "$STAGE_DIR/DEBIAN/control" <<EOF
|
||||
Package: ${PACKAGE_NAME}
|
||||
Version: ${VERSION}
|
||||
Section: graphics
|
||||
Priority: optional
|
||||
Architecture: ${ARCH}
|
||||
Maintainer: slide build
|
||||
Depends: libqt5core5a, libqt5gui5, libqt5widgets5, libqt5network5, libexif12, qt5-image-formats-plugins
|
||||
Description: Lightweight slideshow for photo frames
|
||||
Simple, lightweight slideshow designed for low power devices.
|
||||
EOF
|
||||
|
||||
dpkg-deb --build "$STAGE_DIR" "$DIST_DIR/${PACKAGE_NAME}_${VERSION}_${ARCH}.deb"
|
||||
@@ -48,6 +48,70 @@ void SetJSONBool(bool &value, QJsonObject jsonDoc, const char *key) {
|
||||
}
|
||||
}
|
||||
|
||||
void SetJSONInt(int &value, QJsonObject jsonDoc, const char *key) {
|
||||
if(jsonDoc.contains(key) && jsonDoc[key].isDouble())
|
||||
{
|
||||
value = (int)jsonDoc[key].toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ParseJSONStrings(QJsonObject jsonDoc, const char *key) {
|
||||
std::vector<std::string> values;
|
||||
if(jsonDoc.contains(key) && jsonDoc[key].isArray())
|
||||
{
|
||||
QJsonArray jsonArray = jsonDoc[key].toArray();
|
||||
foreach (const QJsonValue & value, jsonArray)
|
||||
{
|
||||
if (value.isString())
|
||||
{
|
||||
values.push_back(value.toString().toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
ImmichConfig ParseImmichConfigObject(QJsonObject immichJson) {
|
||||
ImmichConfig config;
|
||||
|
||||
std::string url = ParseJSONString(immichJson, "url");
|
||||
std::string apiKey = ParseJSONString(immichJson, "apiKey");
|
||||
if(!url.empty())
|
||||
config.url = url;
|
||||
if(!apiKey.empty())
|
||||
config.apiKey = apiKey;
|
||||
|
||||
std::string size = ParseJSONString(immichJson, "size");
|
||||
if(!size.empty())
|
||||
config.size = size;
|
||||
|
||||
std::string order = ParseJSONString(immichJson, "order");
|
||||
if(!order.empty())
|
||||
config.order = order;
|
||||
|
||||
std::string cachePath = ParseJSONString(immichJson, "cachePath");
|
||||
if(!cachePath.empty())
|
||||
config.cachePath = cachePath;
|
||||
|
||||
std::string albumId = ParseJSONString(immichJson, "albumId");
|
||||
if(!albumId.empty())
|
||||
config.albumIds.push_back(albumId);
|
||||
|
||||
std::vector<std::string> albumIds = ParseJSONStrings(immichJson, "albumIds");
|
||||
if(albumIds.size() > 0)
|
||||
config.albumIds = albumIds;
|
||||
|
||||
SetJSONInt(config.pageSize, immichJson, "pageSize");
|
||||
SetJSONInt(config.maxAssets, immichJson, "maxAssets");
|
||||
SetJSONInt(config.cacheMaxMB, immichJson, "cacheMaxMB");
|
||||
SetJSONBool(config.includeArchived, immichJson, "includeArchived");
|
||||
|
||||
if(!config.url.empty() && !config.apiKey.empty())
|
||||
config.enabled = true;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
Config loadConfiguration(const std::string &configFilePath, const Config ¤tConfig) {
|
||||
if(configFilePath.empty())
|
||||
{
|
||||
@@ -184,6 +248,11 @@ QVector<PathEntry> parsePathEntry(QJsonObject &jsonMainDoc, bool baseRecursive,
|
||||
|
||||
SetJSONBool(entry.baseDisplayOptions.fitAspectAxisToWindow, schedulerJson, "stretch");
|
||||
|
||||
if(schedulerJson.contains("immich") && schedulerJson["immich"].isObject())
|
||||
{
|
||||
entry.immich = ParseImmichConfigObject(schedulerJson["immich"].toObject());
|
||||
}
|
||||
|
||||
std::string pathString = ParseJSONString(schedulerJson, "path");
|
||||
if(!pathString.empty()) {
|
||||
entry.path = pathString;
|
||||
@@ -265,6 +334,10 @@ AppConfig loadAppConfiguration(const AppConfig &commandLineConfig) {
|
||||
entry.recursive = baseRecursive;
|
||||
entry.sorted = baseSorted;
|
||||
entry.shuffle = baseShuffle;
|
||||
if(jsonDoc.contains("immich") && jsonDoc["immich"].isObject())
|
||||
{
|
||||
entry.immich = ParseImmichConfigObject(jsonDoc["immich"].toObject());
|
||||
}
|
||||
std::string pathString = ParseJSONString(jsonDoc, "path");
|
||||
if(!pathString.empty())
|
||||
{
|
||||
|
||||
@@ -4,6 +4,50 @@
|
||||
#include <QDateTime>
|
||||
#include "imagestructs.h"
|
||||
#include <QVector>
|
||||
#include <vector>
|
||||
|
||||
struct ImmichConfig {
|
||||
bool enabled = false;
|
||||
std::string url = "";
|
||||
std::string apiKey = "";
|
||||
std::vector<std::string> albumIds;
|
||||
std::string size = "fullsize";
|
||||
std::string order = "desc";
|
||||
int pageSize = 200;
|
||||
int maxAssets = 1000;
|
||||
std::string cachePath = "";
|
||||
int cacheMaxMB = 512;
|
||||
bool includeArchived = false;
|
||||
|
||||
bool operator==(const ImmichConfig &b) const
|
||||
{
|
||||
if (enabled != b.enabled)
|
||||
return false;
|
||||
if (url != b.url || apiKey != b.apiKey)
|
||||
return false;
|
||||
if (size != b.size || order != b.order)
|
||||
return false;
|
||||
if (pageSize != b.pageSize || maxAssets != b.maxAssets)
|
||||
return false;
|
||||
if (cachePath != b.cachePath || cacheMaxMB != b.cacheMaxMB)
|
||||
return false;
|
||||
if (includeArchived != b.includeArchived)
|
||||
return false;
|
||||
if (albumIds.size() != b.albumIds.size())
|
||||
return false;
|
||||
for (size_t i = 0; i < albumIds.size(); ++i)
|
||||
{
|
||||
if (albumIds[i] != b.albumIds[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool operator!=(const ImmichConfig &b) const
|
||||
{
|
||||
return !operator==(b);
|
||||
}
|
||||
};
|
||||
|
||||
// configuration options that apply to an image/folder of images
|
||||
struct Config {
|
||||
@@ -21,6 +65,7 @@ struct PathEntry {
|
||||
std::string imageList = "";
|
||||
bool exclusive = false; // only use this entry when it is valid, skip others
|
||||
|
||||
ImmichConfig immich;
|
||||
bool recursive = false;
|
||||
bool shuffle = false;
|
||||
bool sorted = false;
|
||||
@@ -40,6 +85,8 @@ struct PathEntry {
|
||||
return true;
|
||||
if (b.path != path || b.imageList != imageList)
|
||||
return true;
|
||||
if (b.immich != immich)
|
||||
return true;
|
||||
if (b.baseDisplayOptions.timeWindows.count() != baseDisplayOptions.timeWindows.count())
|
||||
return true;
|
||||
for(int i = 0; i < baseDisplayOptions.timeWindows.count(); ++i)
|
||||
|
||||
337
src/immichclient.cpp
Normal file
337
src/immichclient.cpp
Normal file
@@ -0,0 +1,337 @@
|
||||
#include "immichclient.h"
|
||||
#include "logger.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QSaveFile>
|
||||
#include <QTimer>
|
||||
#include <QUrlQuery>
|
||||
|
||||
namespace {
|
||||
const int kMetadataTimeoutMs = 15000;
|
||||
const int kAssetTimeoutMs = 30000;
|
||||
}
|
||||
|
||||
ImmichClient::ImmichClient(const ImmichConfig &configIn)
|
||||
: config(configIn)
|
||||
{}
|
||||
|
||||
QUrl ImmichClient::apiUrl(const QString &path) const
|
||||
{
|
||||
QString base = QString::fromStdString(config.url).trimmed();
|
||||
if (base.endsWith("/"))
|
||||
base.chop(1);
|
||||
if (!base.endsWith("/api"))
|
||||
base += "/api";
|
||||
return QUrl(base + path);
|
||||
}
|
||||
|
||||
QNetworkRequest ImmichClient::makeRequest(const QUrl &url) const
|
||||
{
|
||||
QNetworkRequest request(url);
|
||||
request.setRawHeader("x-api-key", QByteArray::fromStdString(config.apiKey));
|
||||
return request;
|
||||
}
|
||||
|
||||
bool ImmichClient::waitForReply(QNetworkReply *reply, QByteArray &data, QString *contentType, int timeoutMs)
|
||||
{
|
||||
QEventLoop loop;
|
||||
QTimer timer;
|
||||
timer.setSingleShot(true);
|
||||
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
timer.start(timeoutMs);
|
||||
loop.exec();
|
||||
|
||||
if (!timer.isActive())
|
||||
{
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (contentType)
|
||||
*contentType = reply->header(QNetworkRequest::ContentTypeHeader).toString();
|
||||
data = reply->readAll();
|
||||
bool ok = reply->error() == QNetworkReply::NoError;
|
||||
if (!ok)
|
||||
{
|
||||
Log("Immich request failed: ", reply->errorString().toStdString());
|
||||
}
|
||||
reply->deleteLater();
|
||||
return ok;
|
||||
}
|
||||
|
||||
QByteArray ImmichClient::postJson(const QUrl &url, const QJsonObject &body, QString *contentType, int timeoutMs)
|
||||
{
|
||||
QNetworkRequest request = makeRequest(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
QNetworkReply *reply = manager.post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
|
||||
QByteArray data;
|
||||
if (!waitForReply(reply, data, contentType, timeoutMs))
|
||||
return QByteArray();
|
||||
return data;
|
||||
}
|
||||
|
||||
QByteArray ImmichClient::getBytes(const QUrl &url, QString *contentType, int timeoutMs)
|
||||
{
|
||||
QNetworkRequest request = makeRequest(url);
|
||||
request.setRawHeader("Accept", "*/*");
|
||||
QNetworkReply *reply = manager.get(request);
|
||||
QByteArray data;
|
||||
if (!waitForReply(reply, data, contentType, timeoutMs))
|
||||
return QByteArray();
|
||||
return data;
|
||||
}
|
||||
|
||||
QVector<ImmichAsset> ImmichClient::fetchAssets()
|
||||
{
|
||||
QVector<ImmichAsset> assets;
|
||||
if (!config.enabled)
|
||||
{
|
||||
Log("Immich config is missing url or apiKey.");
|
||||
return assets;
|
||||
}
|
||||
|
||||
int pageSize = config.pageSize > 0 ? config.pageSize : 200;
|
||||
int maxAssets = config.maxAssets;
|
||||
bool triedZero = false;
|
||||
int page = 1;
|
||||
|
||||
while (true)
|
||||
{
|
||||
QJsonObject body;
|
||||
body["page"] = page;
|
||||
body["size"] = pageSize;
|
||||
body["type"] = "IMAGE";
|
||||
body["order"] = QString::fromStdString(config.order);
|
||||
if (config.includeArchived)
|
||||
body["withArchived"] = true;
|
||||
if (config.albumIds.size() > 0)
|
||||
{
|
||||
QJsonArray ids;
|
||||
for (const auto &id : config.albumIds)
|
||||
ids.append(QString::fromStdString(id));
|
||||
body["albumIds"] = ids;
|
||||
}
|
||||
|
||||
QByteArray response = postJson(apiUrl("/search/metadata"), body, nullptr, kMetadataTimeoutMs);
|
||||
if (response.isEmpty())
|
||||
break;
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response);
|
||||
if (!doc.isObject())
|
||||
break;
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QJsonObject assetsObj = root["assets"].toObject();
|
||||
QJsonArray items = assetsObj["items"].toArray();
|
||||
int total = assetsObj["total"].toInt();
|
||||
if (items.isEmpty())
|
||||
{
|
||||
if (total > 0 && page == 1 && !triedZero)
|
||||
{
|
||||
triedZero = true;
|
||||
page = 0;
|
||||
continue;
|
||||
}
|
||||
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();
|
||||
assets.append(asset);
|
||||
if (maxAssets > 0 && assets.size() >= maxAssets)
|
||||
return assets;
|
||||
}
|
||||
|
||||
if (items.size() < pageSize)
|
||||
break;
|
||||
page += 1;
|
||||
}
|
||||
|
||||
return assets;
|
||||
}
|
||||
|
||||
bool ImmichClient::downloadAsset(const QString &assetId, QByteArray &data, QString &contentType)
|
||||
{
|
||||
if (!config.enabled)
|
||||
return false;
|
||||
|
||||
QString size = QString::fromStdString(config.size).trimmed().toLower();
|
||||
if (size.isEmpty())
|
||||
size = "fullsize";
|
||||
|
||||
QUrl url;
|
||||
if (size == "original" || size == "download")
|
||||
{
|
||||
url = apiUrl("/assets/" + assetId + "/download");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (size != "fullsize" && size != "preview" && size != "thumbnail")
|
||||
{
|
||||
Log("Immich size '", size.toStdString(), "' not recognized. Defaulting to fullsize.");
|
||||
size = "fullsize";
|
||||
}
|
||||
url = apiUrl("/assets/" + assetId + "/thumbnail");
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("size", size);
|
||||
url.setQuery(query);
|
||||
}
|
||||
|
||||
QByteArray payload = getBytes(url, &contentType, kAssetTimeoutMs);
|
||||
if (payload.isEmpty())
|
||||
return false;
|
||||
|
||||
data = payload;
|
||||
return true;
|
||||
}
|
||||
|
||||
ImmichAssetCache::ImmichAssetCache(const ImmichConfig &config)
|
||||
{
|
||||
QString rawPath = QString::fromStdString(config.cachePath);
|
||||
cacheDirPath = resolveCachePath(rawPath);
|
||||
if (config.cacheMaxMB > 0)
|
||||
cacheMaxBytes = static_cast<qint64>(config.cacheMaxMB) * 1024 * 1024;
|
||||
}
|
||||
|
||||
QString ImmichAssetCache::resolveCachePath(const QString &rawPath) const
|
||||
{
|
||||
if (rawPath.isEmpty())
|
||||
{
|
||||
return QDir::homePath() + "/.cache/slide/immich";
|
||||
}
|
||||
if (rawPath.startsWith("~"))
|
||||
{
|
||||
return QDir::homePath() + rawPath.mid(1);
|
||||
}
|
||||
return rawPath;
|
||||
}
|
||||
|
||||
void ImmichAssetCache::ensureCacheDir() const
|
||||
{
|
||||
QDir dir(cacheDirPath);
|
||||
if (!dir.exists())
|
||||
dir.mkpath(".");
|
||||
}
|
||||
|
||||
QString ImmichAssetCache::findExisting(const QString &assetId) const
|
||||
{
|
||||
QDir dir(cacheDirPath);
|
||||
QStringList matches = dir.entryList(QStringList() << (assetId + "_*"), QDir::Files, QDir::Time);
|
||||
if (matches.isEmpty())
|
||||
return "";
|
||||
return dir.filePath(matches.first());
|
||||
}
|
||||
|
||||
QString ImmichAssetCache::sanitizeFileName(const QString &name) const
|
||||
{
|
||||
QString safe = name;
|
||||
safe.replace(QRegularExpression("[^A-Za-z0-9_.-]"), "_");
|
||||
if (safe.isEmpty())
|
||||
safe = "asset";
|
||||
return safe;
|
||||
}
|
||||
|
||||
QString ImmichAssetCache::extensionForContentType(const QString &contentType) const
|
||||
{
|
||||
if (contentType.contains("jpeg", Qt::CaseInsensitive))
|
||||
return "jpg";
|
||||
if (contentType.contains("png", Qt::CaseInsensitive))
|
||||
return "png";
|
||||
if (contentType.contains("webp", Qt::CaseInsensitive))
|
||||
return "webp";
|
||||
if (contentType.contains("gif", Qt::CaseInsensitive))
|
||||
return "gif";
|
||||
return "img";
|
||||
}
|
||||
|
||||
qint64 ImmichAssetCache::calculateCacheSize() const
|
||||
{
|
||||
QDir dir(cacheDirPath);
|
||||
QFileInfoList files = dir.entryInfoList(QDir::Files, QDir::Time);
|
||||
qint64 total = 0;
|
||||
for (const auto &info : files)
|
||||
total += info.size();
|
||||
return total;
|
||||
}
|
||||
|
||||
void ImmichAssetCache::enforceCacheLimit()
|
||||
{
|
||||
if (cacheMaxBytes <= 0)
|
||||
return;
|
||||
|
||||
QDir dir(cacheDirPath);
|
||||
QFileInfoList files = dir.entryInfoList(QDir::Files, QDir::Time);
|
||||
qint64 total = 0;
|
||||
for (const auto &info : files)
|
||||
total += info.size();
|
||||
|
||||
for (int i = files.size() - 1; i >= 0 && total > cacheMaxBytes; --i)
|
||||
{
|
||||
total -= files[i].size();
|
||||
QFile::remove(files[i].filePath());
|
||||
}
|
||||
cacheSizeBytes = total;
|
||||
cacheSizeKnown = true;
|
||||
}
|
||||
|
||||
QString ImmichAssetCache::getCachedPath(const QString &assetId, const QString &assetName, ImmichClient &client)
|
||||
{
|
||||
ensureCacheDir();
|
||||
|
||||
QString existing = findExisting(assetId);
|
||||
if (!existing.isEmpty())
|
||||
return existing;
|
||||
|
||||
QByteArray data;
|
||||
QString contentType;
|
||||
if (!client.downloadAsset(assetId, data, contentType))
|
||||
return "";
|
||||
|
||||
QString safeName = sanitizeFileName(assetName);
|
||||
QString extension = extensionForContentType(contentType);
|
||||
QString filename = assetId + "_" + safeName + "." + extension;
|
||||
|
||||
QDir dir(cacheDirPath);
|
||||
QString filePath = dir.filePath(filename);
|
||||
|
||||
QSaveFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
return "";
|
||||
file.write(data);
|
||||
if (!file.commit())
|
||||
return "";
|
||||
|
||||
if (cacheMaxBytes > 0)
|
||||
{
|
||||
if (!cacheSizeKnown)
|
||||
{
|
||||
cacheSizeBytes = calculateCacheSize();
|
||||
cacheSizeKnown = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
cacheSizeBytes += data.size();
|
||||
}
|
||||
if (cacheSizeBytes > cacheMaxBytes)
|
||||
enforceCacheLimit();
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
56
src/immichclient.h
Normal file
56
src/immichclient.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#ifndef IMMICHCLIENT_H
|
||||
#define IMMICHCLIENT_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QVector>
|
||||
|
||||
#include "appconfig.h"
|
||||
|
||||
struct ImmichAsset {
|
||||
QString id;
|
||||
QString originalFileName;
|
||||
};
|
||||
|
||||
class ImmichClient {
|
||||
public:
|
||||
explicit ImmichClient(const ImmichConfig &config);
|
||||
QVector<ImmichAsset> fetchAssets();
|
||||
bool downloadAsset(const QString &assetId, QByteArray &data, QString &contentType);
|
||||
|
||||
private:
|
||||
QUrl apiUrl(const QString &path) const;
|
||||
QNetworkRequest makeRequest(const QUrl &url) const;
|
||||
QByteArray postJson(const QUrl &url, const QJsonObject &body, QString *contentType, int timeoutMs);
|
||||
QByteArray getBytes(const QUrl &url, QString *contentType, int timeoutMs);
|
||||
bool waitForReply(QNetworkReply *reply, QByteArray &data, QString *contentType, int timeoutMs);
|
||||
|
||||
ImmichConfig config;
|
||||
QNetworkAccessManager manager;
|
||||
};
|
||||
|
||||
class ImmichAssetCache {
|
||||
public:
|
||||
explicit ImmichAssetCache(const ImmichConfig &config);
|
||||
QString getCachedPath(const QString &assetId, const QString &assetName, ImmichClient &client);
|
||||
|
||||
private:
|
||||
QString resolveCachePath(const QString &rawPath) const;
|
||||
QString findExisting(const QString &assetId) const;
|
||||
QString sanitizeFileName(const QString &name) const;
|
||||
QString extensionForContentType(const QString &contentType) const;
|
||||
void ensureCacheDir() const;
|
||||
void enforceCacheLimit();
|
||||
qint64 calculateCacheSize() const;
|
||||
|
||||
QString cacheDirPath;
|
||||
qint64 cacheMaxBytes = 0;
|
||||
bool cacheSizeKnown = false;
|
||||
qint64 cacheSizeBytes = 0;
|
||||
};
|
||||
|
||||
#endif // IMMICHCLIENT_H
|
||||
47
src/immichpathtraverser.cpp
Normal file
47
src/immichpathtraverser.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "immichpathtraverser.h"
|
||||
#include "logger.h"
|
||||
|
||||
ImmichPathTraverser::ImmichPathTraverser(const ImmichConfig &configIn)
|
||||
: PathTraverser(""),
|
||||
config(configIn),
|
||||
client(configIn),
|
||||
cache(configIn)
|
||||
{
|
||||
loadAssets();
|
||||
}
|
||||
|
||||
ImmichPathTraverser::~ImmichPathTraverser() {}
|
||||
|
||||
void ImmichPathTraverser::loadAssets()
|
||||
{
|
||||
assetIds.clear();
|
||||
assetNames.clear();
|
||||
QVector<ImmichAsset> assets = client.fetchAssets();
|
||||
for (const auto &asset : assets)
|
||||
{
|
||||
if (asset.id.isEmpty())
|
||||
continue;
|
||||
assetIds.append(asset.id);
|
||||
assetNames.insert(asset.id, asset.originalFileName);
|
||||
}
|
||||
Log("Immich assets loaded: ", assetIds.size());
|
||||
}
|
||||
|
||||
QStringList ImmichPathTraverser::getImages() const
|
||||
{
|
||||
return assetIds;
|
||||
}
|
||||
|
||||
const std::string ImmichPathTraverser::getImagePath(const std::string image) const
|
||||
{
|
||||
QString assetId = QString::fromStdString(image);
|
||||
QString name = assetNames.value(assetId);
|
||||
QString path = cache.getCachedPath(assetId, name, client);
|
||||
return path.toStdString();
|
||||
}
|
||||
|
||||
ImageDisplayOptions ImmichPathTraverser::UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& options) const
|
||||
{
|
||||
Q_UNUSED(filename);
|
||||
return options;
|
||||
}
|
||||
28
src/immichpathtraverser.h
Normal file
28
src/immichpathtraverser.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef IMMICHPATHTRAVERSER_H
|
||||
#define IMMICHPATHTRAVERSER_H
|
||||
|
||||
#include "pathtraverser.h"
|
||||
#include "immichclient.h"
|
||||
|
||||
#include <QHash>
|
||||
|
||||
class ImmichPathTraverser : public PathTraverser
|
||||
{
|
||||
public:
|
||||
ImmichPathTraverser(const ImmichConfig &config);
|
||||
virtual ~ImmichPathTraverser();
|
||||
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;
|
||||
|
||||
private:
|
||||
void loadAssets();
|
||||
|
||||
ImmichConfig config;
|
||||
mutable ImmichClient client;
|
||||
mutable ImmichAssetCache cache;
|
||||
QStringList assetIds;
|
||||
QHash<QString, QString> assetNames;
|
||||
};
|
||||
|
||||
#endif // IMMICHPATHTRAVERSER_H
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "imageselector.h"
|
||||
#include "imageswitcher.h"
|
||||
#include "pathtraverser.h"
|
||||
#include "immichpathtraverser.h"
|
||||
#include "overlay.h"
|
||||
#include "appconfig.h"
|
||||
#include "logger.h"
|
||||
@@ -157,7 +158,11 @@ void ConfigureWindowFromSettings(MainWindow &w, const AppConfig &appConfig)
|
||||
std::unique_ptr<ImageSelector> GetSelectorForConfig(const PathEntry& path)
|
||||
{
|
||||
std::unique_ptr<PathTraverser> pathTraverser;
|
||||
if (!path.imageList.empty())
|
||||
if (path.immich.enabled)
|
||||
{
|
||||
pathTraverser = std::unique_ptr<PathTraverser>(new ImmichPathTraverser(path.immich));
|
||||
}
|
||||
else if (!path.imageList.empty())
|
||||
{
|
||||
pathTraverser = std::unique_ptr<PathTraverser>(new ImageListPathTraverser(path.imageList));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
#-------------------------------------------------
|
||||
|
||||
QT += core gui
|
||||
QT += core gui network
|
||||
CONFIG += qt
|
||||
CONFIG += debug
|
||||
CONFIG += c++1z
|
||||
@@ -35,6 +35,8 @@ SOURCES += \
|
||||
mainwindow.cpp \
|
||||
imageswitcher.cpp \
|
||||
pathtraverser.cpp \
|
||||
immichpathtraverser.cpp \
|
||||
immichclient.cpp \
|
||||
overlay.cpp \
|
||||
imageselector.cpp \
|
||||
appconfig.cpp \
|
||||
@@ -45,6 +47,8 @@ HEADERS += \
|
||||
mainwindow.h \
|
||||
imageselector.h \
|
||||
pathtraverser.h \
|
||||
immichpathtraverser.h \
|
||||
immichclient.h \
|
||||
overlay.h \
|
||||
imageswitcher.h \
|
||||
imagestructs.h \
|
||||
|
||||
Reference in New Issue
Block a user