Add read-only Synology Photos people filtering
This commit is contained in:
@@ -127,3 +127,19 @@ no thumbnails are generated on the NAS, and the ordinary size limit still applie
|
||||
to the selected preview. The catalog marks these entries `provider_preview: true`.
|
||||
If the preview is missing, the oversized original remains excluded. The folder
|
||||
caption still comes from the original curated directory.
|
||||
|
||||
## Synology Photos 人物筛选 / People filtering
|
||||
|
||||
可选的只读集成使用 Synology Photos **共享空间**的人物识别结果,不运行新的识别模型,也不修改人物分组或照片。个人空间暂不支持。
|
||||
|
||||
1. 将共享图库(如 `/volume1/photo`)挂载为 `/photos`,保持只读。
|
||||
2. 在 `.env` 配置 `SYNOLOGY_PHOTOS_URL=https://NAS_IP:9115` 和 `SYNOLOGY_PHOTOS_CERT_SHA256`(服务实际 TLS 证书的 SHA-256 指纹,从可信 NAS 配置核实)。重建容器后生效。指纹变化时必须重新核实;每次连接在发送登录信息前校验同一个 TLS socket 的证书。
|
||||
3. 图库 → 编辑照片数据源 → 连接 / 选择人物。管理员输入 NAS 用户名、密码;只保存 session 到 `/config/synology-photos-session.json`(0600),不保存密码。会话过期后需要重新连接;断开连接删除本地 session。页面没有 password/token 回显。
|
||||
4. 选择人物,启用筛选,选择“任意”或“同时包含所有”。默认只列出已命名人物,可搜索 ID、展开未命名分组。至少选择一个人物。
|
||||
5. 与当前本地目录、日期文件夹规则取交集,再随机选图。原文件夹标题、日期、地点仍保留。不匹配返回空图库;预览 / 定时任务失败,不回退到全图库。
|
||||
|
||||
接口:`GET/POST/DELETE /api/v1/integrations/synology-photos`(管理员);`GET /api/v1/integrations/synology-photos/people`(管理员)。Source 新增 `people_filter`、`person_ids`、`person_match: any|all`。普通图片读取权限只能使用已配置数据源,不能登录或查询人物目录。
|
||||
|
||||
只允许 Auth login/logout、共享空间 Person.list、Item.list、Folder.get。人物照片按 500 条分页,成员路径缓存 15 分钟,源目录缓存 60 秒;显式刷新会重新查询。无后台高频轮询,不下载全图库原图。查询失败和登录失效明确报错。大型人物相册首次查询可能需要数十秒。
|
||||
|
||||
This optional integration reads the existing shared-space People catalog. It does not modify Photos or perform face inference. Login stores only a protected session, never the NAS password. Certificate pinning happens before credentials are sent. Select one or more people and match any/all, intersect with the existing curated folder rules, then randomize. Empty matches and expired sessions fail closed. Person membership caches for 15 minutes; explicit refresh re-queries. This improves relevance, not blur/closed-eye detection. Recognition can miss faces. Personal-space filtering and automatic photo-quality scoring are not implemented.
|
||||
|
||||
@@ -3061,6 +3061,572 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/integrations/synology-photos": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Photo sources"
|
||||
],
|
||||
"summary": "Photos Connection",
|
||||
"operationId": "photos_connection_api_v1_integrations_synology_photos_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PhotosConnection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"413": {
|
||||
"description": "Content Too Large",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Too Many Requests",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Bad Gateway",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Service Unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"HTTPBearer": []
|
||||
},
|
||||
{
|
||||
"AccountSession": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Photo sources"
|
||||
],
|
||||
"summary": "Connect Photos",
|
||||
"operationId": "connect_photos_api_v1_integrations_synology_photos_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PhotosLogin"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PhotosConnection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"413": {
|
||||
"description": "Content Too Large",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Too Many Requests",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Bad Gateway",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Service Unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"HTTPBearer": []
|
||||
},
|
||||
{
|
||||
"AccountSession": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Photo sources"
|
||||
],
|
||||
"summary": "Disconnect Photos",
|
||||
"operationId": "disconnect_photos_api_v1_integrations_synology_photos_delete",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PhotosConnection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"413": {
|
||||
"description": "Content Too Large",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Too Many Requests",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Bad Gateway",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Service Unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"HTTPBearer": []
|
||||
},
|
||||
{
|
||||
"AccountSession": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/integrations/synology-photos/people": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Photo sources"
|
||||
],
|
||||
"summary": "Photos People",
|
||||
"operationId": "photos_people_api_v1_integrations_synology_photos_people_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PhotosPerson"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Response Photos People Api V1 Integrations Synology Photos People Get"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"413": {
|
||||
"description": "Content Too Large",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Too Many Requests",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"502": {
|
||||
"description": "Bad Gateway",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Service Unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"HTTPBearer": []
|
||||
},
|
||||
{
|
||||
"AccountSession": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/photo-sources": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -10436,6 +11002,28 @@
|
||||
"title": "Folder Caption",
|
||||
"default": false
|
||||
},
|
||||
"people_filter": {
|
||||
"type": "boolean",
|
||||
"title": "People Filter",
|
||||
"default": false
|
||||
},
|
||||
"person_ids": {
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "array",
|
||||
"maxItems": 50,
|
||||
"title": "Person Ids"
|
||||
},
|
||||
"person_match": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"any",
|
||||
"all"
|
||||
],
|
||||
"title": "Person Match",
|
||||
"default": "any"
|
||||
},
|
||||
"credential_env": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -10512,6 +11100,28 @@
|
||||
"title": "Folder Caption",
|
||||
"default": false
|
||||
},
|
||||
"people_filter": {
|
||||
"type": "boolean",
|
||||
"title": "People Filter",
|
||||
"default": false
|
||||
},
|
||||
"person_ids": {
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "array",
|
||||
"maxItems": 50,
|
||||
"title": "Person Ids"
|
||||
},
|
||||
"person_match": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"any",
|
||||
"all"
|
||||
],
|
||||
"title": "Person Match",
|
||||
"default": "any"
|
||||
},
|
||||
"credential_env": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -10539,6 +11149,79 @@
|
||||
],
|
||||
"title": "PhotoSourceView"
|
||||
},
|
||||
"PhotosConnection": {
|
||||
"properties": {
|
||||
"configured": {
|
||||
"type": "boolean",
|
||||
"title": "Configured"
|
||||
},
|
||||
"connected": {
|
||||
"type": "boolean",
|
||||
"title": "Connected"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"configured",
|
||||
"connected"
|
||||
],
|
||||
"title": "PhotosConnection"
|
||||
},
|
||||
"PhotosLogin": {
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string",
|
||||
"maxLength": 120,
|
||||
"minLength": 1,
|
||||
"title": "Username"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"maxLength": 256,
|
||||
"minLength": 1,
|
||||
"format": "password",
|
||||
"title": "Password",
|
||||
"writeOnly": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"username",
|
||||
"password"
|
||||
],
|
||||
"title": "PhotosLogin"
|
||||
},
|
||||
"PhotosPerson": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"title": "Id"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"title": "Count",
|
||||
"default": 0
|
||||
},
|
||||
"named": {
|
||||
"type": "boolean",
|
||||
"title": "Named",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"title": "PhotosPerson"
|
||||
},
|
||||
"Playlist": {
|
||||
"properties": {
|
||||
"device_id": {
|
||||
|
||||
Reference in New Issue
Block a user