1 Commits

Author SHA1 Message Date
fc0ec58636 [ci skip] add ability to skip immich photos via tag 2026-02-02 14:11:38 +11:00
5 changed files with 128 additions and 2 deletions

View File

@@ -206,6 +206,7 @@ Example (single source):
"maxAssets": 1000,
"refreshSeconds": 300,
"skipRetrySeconds": 3600,
"skipTags": ["frame-ignore"],
"cachePath": "~/.cache/slide/immich",
"cacheMaxMB": 512,
"includeArchived": false
@@ -237,8 +238,12 @@ Immich settings:
* `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"]`).
* `skipTags`: optional list of tag names to exclude (case-insensitive).
* `skipTagIds`: optional list of tag IDs to exclude (case-insensitive).
* `size`: `"fullsize"`, `"preview"`, `"thumbnail"`, or `"original"` (original uses the download endpoint).
* `order`: `"asc"` or `"desc"` ordering for asset search.
Note: tag filtering requires Immich to include tag data in search results. If your server doesnt return tags on `/search/metadata`, `skipTags`/`skipTagIds` wont apply.
* `pageSize`: assets fetched per page.
* `maxAssets`: cap on total assets fetched (0 means no cap).
* `refreshSeconds`: refresh interval for reloading Immich assets (0 disables).

View File

@@ -84,6 +84,19 @@ std::vector<std::string> NormalizeExtensions(const std::vector<std::string> &val
return normalized;
}
std::vector<std::string> NormalizeStrings(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;
@@ -135,6 +148,22 @@ ImmichConfig ParseImmichConfigObject(QJsonObject immichJson) {
if(allowedExtensions.size() > 0)
config.allowedExtensions = NormalizeExtensions(allowedExtensions);
std::vector<std::string> skipTags = ParseJSONStrings(immichJson, "skipTags");
if(skipTags.size() == 0)
skipTags = ParseJSONStrings(immichJson, "excludeTags");
std::string skipTag = ParseJSONString(immichJson, "skipTag");
if(!skipTag.empty())
skipTags.push_back(skipTag);
if(skipTags.size() > 0)
config.skipTags = NormalizeStrings(skipTags);
std::vector<std::string> skipTagIds = ParseJSONStrings(immichJson, "skipTagIds");
std::string skipTagId = ParseJSONString(immichJson, "skipTagId");
if(!skipTagId.empty())
skipTagIds.push_back(skipTagId);
if(skipTagIds.size() > 0)
config.skipTagIds = NormalizeStrings(skipTagIds);
SetJSONInt(config.pageSize, immichJson, "pageSize");
SetJSONInt(config.maxAssets, immichJson, "maxAssets");
SetJSONInt(config.refreshSeconds, immichJson, "refreshSeconds");

View File

@@ -14,6 +14,8 @@ struct ImmichConfig {
std::vector<std::string> albumIds;
std::vector<std::string> personIds;
std::vector<std::string> allowedExtensions;
std::vector<std::string> skipTags;
std::vector<std::string> skipTagIds;
std::string size = "fullsize";
std::string order = "desc";
int pageSize = 200;
@@ -61,6 +63,20 @@ struct ImmichConfig {
if (allowedExtensions[i] != b.allowedExtensions[i])
return false;
}
if (skipTags.size() != b.skipTags.size())
return false;
for (size_t i = 0; i < skipTags.size(); ++i)
{
if (skipTags[i] != b.skipTags[i])
return false;
}
if (skipTagIds.size() != b.skipTagIds.size())
return false;
for (size_t i = 0; i < skipTagIds.size(); ++i)
{
if (skipTagIds[i] != b.skipTagIds[i])
return false;
}
if (refreshSeconds != b.refreshSeconds)
return false;
if (skipRetrySeconds != b.skipRetrySeconds)

View File

@@ -178,7 +178,9 @@ QVector<ImmichAsset> ImmichClient::fetchAssetsBySearch()
", userId=", config.userId,
", albumIds=", config.albumIds.size(),
", personIds=", config.personIds.size(),
", allowedExtensions=", config.allowedExtensions.size());
", allowedExtensions=", config.allowedExtensions.size(),
", skipTags=", config.skipTags.size(),
", skipTagIds=", config.skipTagIds.size());
}
auto fetchPage = [&](int pageIndex, const QString &userKey) -> QByteArray {
@@ -288,6 +290,11 @@ QVector<ImmichAsset> ImmichClient::fetchAssetsBySearch()
asset.id = id;
asset.originalFileName = item["originalFileName"].toString();
asset.exifDateTime = ExtractAssetDateTime(item);
if (shouldSkipByTag(item))
{
Log("Immich skip by tag: ", asset.originalFileName.toStdString());
continue;
}
if (!extensionAllowed(asset.originalFileName))
{
Log("Immich skip by extension: ", asset.originalFileName.toStdString());
@@ -420,6 +427,71 @@ bool ImmichClient::extensionAllowed(const QString &filename) const
return false;
}
bool ImmichClient::shouldSkipByTag(const QJsonObject &item) const
{
if (config.skipTags.empty() && config.skipTagIds.empty())
return false;
auto matchesName = [&](const QString &name) -> bool {
if (name.isEmpty())
return false;
QString lowered = name.toLower();
for (const auto &tag : config.skipTags)
{
if (lowered == QString::fromStdString(tag))
return true;
}
return false;
};
auto matchesId = [&](const QString &id) -> bool {
if (id.isEmpty())
return false;
QString lowered = id.toLower();
for (const auto &tagId : config.skipTagIds)
{
if (lowered == QString::fromStdString(tagId))
return true;
}
return false;
};
if (item.contains("tagIds") && item["tagIds"].isArray())
{
QJsonArray tagIds = item["tagIds"].toArray();
for (const auto &value : tagIds)
{
if (value.isString() && matchesId(value.toString()))
return true;
}
}
if (item.contains("tags") && item["tags"].isArray())
{
QJsonArray tags = item["tags"].toArray();
for (const auto &value : tags)
{
if (value.isString())
{
if (matchesName(value.toString()))
return true;
continue;
}
if (!value.isObject())
continue;
QJsonObject tagObj = value.toObject();
if (matchesId(tagObj["id"].toString()))
return true;
if (matchesName(tagObj["name"].toString()))
return true;
if (matchesName(tagObj["value"].toString()))
return true;
}
}
return false;
}
bool ImmichClient::downloadAsset(const QString &assetId, QByteArray &data, QString &contentType)
{
return downloadAssetWithSize(assetId, QString::fromStdString(config.size), data, contentType);
@@ -655,7 +727,10 @@ QString ImmichAssetCache::getCachedPath(const QString &assetId, const QString &a
}
}
QString safeName = sanitizeFileName(assetName);
QString baseName = QFileInfo(assetName).completeBaseName();
if (baseName.isEmpty())
baseName = assetName;
QString safeName = sanitizeFileName(baseName);
QString extension = detectedExt;
if (extension.isEmpty())
extension = extensionForContentType(contentType);

View File

@@ -27,6 +27,7 @@ class ImmichClient {
private:
QVector<ImmichAsset> fetchAssetsBySearch();
QVector<ImmichAsset> fetchAssetsByUser();
bool shouldSkipByTag(const QJsonObject &item) const;
bool extensionAllowed(const QString &filename) const;
QUrl apiUrl(const QString &path) const;
QNetworkRequest makeRequest(const QUrl &url) const;