205 lines
14 KiB
Markdown
205 lines
14 KiB
Markdown
# Photo sources
|
||
|
||
Photo sources are independent read-only providers. They never call a frame driver.
|
||
SQLite persists source definitions and Scheduler jobs; passwords are not stored there.
|
||
The shared DisplayService handles fitting, upload, idempotency, overrides and restore.
|
||
|
||
## NAS local folder
|
||
|
||
Add the actual existing photo directory to `.env`:
|
||
|
||
```dotenv
|
||
NAS_PHOTOS_DIR=/volume2/photo
|
||
# Optional: existing NAS group allowed to read that folder (check its ACL).
|
||
# NAS_PHOTOS_GID=101
|
||
```
|
||
|
||
The path above is an example, not a detected directory. Enable the optional mount:
|
||
|
||
```sh
|
||
docker compose -f docker-compose.yml -f deploy/docker-compose.photos.yml config -q
|
||
docker compose -f docker-compose.yml -f deploy/docker-compose.photos.yml up -d
|
||
```
|
||
|
||
The folder is mounted **read-only** at `/photos`. The existing container user must
|
||
already have read/traverse permission. If the Synology ACL grants a group access,
|
||
set `NAS_PHOTOS_GID` to that existing group ID; the optional Compose overlay adds
|
||
only container group membership, keeping UID 10333 and the photo bind mount read-only.
|
||
Group 101 is a deployment example, not a universal default. Frame Flow does not
|
||
change NAS permissions. Unreadable roots return an explicit HTTP 403 rather than
|
||
an empty catalog.
|
||
Missing host folders fail explicitly rather than being created automatically.
|
||
|
||
Open **Gallery → Photo sources → Add source**, choose **NAS local folder**,
|
||
enter a relative folder (`.` for root or `family` for a subfolder).
|
||
Supported formats: JPEG, PNG, WebP. Optional recursion excludes hidden directories,
|
||
Synology `@eaDir` thumbnail trees, and symlinks. Original files remain unchanged.
|
||
|
||
## WebDAV
|
||
|
||
Open Settings and allow the server hostname under Image URL hosts.
|
||
Add a WebDAV source using a plain HTTPS directory URL on port 443.
|
||
The implementation uses read-only `PROPFIND Depth: 1` and `GET`, following
|
||
[RFC 4918](https://www.rfc-editor.org/rfc/rfc4918#section-9.1).
|
||
This version lists images immediately inside one folder; recursive WebDAV traversal
|
||
is not supported. Basic authentication is supported; OAuth, SSO and Digest are not.
|
||
Redirects, private-address endpoints and URL-embedded credentials are rejected.
|
||
|
||
For authentication, reference an environment variable such as
|
||
`FRAME_FLOW_SOURCE_CLOUD`; set its value to a JSON object containing `username`
|
||
and `password`, or set `FRAME_FLOW_SOURCE_CLOUD_FILE` to a mounted Secret file.
|
||
Compose's env_file supplies the variable; a Secret file belongs in the existing
|
||
`/config` mount with restricted permissions and container read access. Never commit
|
||
credentials. The UI/API stores only the variable name.
|
||
|
||
## Use
|
||
|
||
- **Browse photos** returns paginated metadata (50 items/page), without fetching originals.
|
||
- **Preview random photo** imports one selected photo into local Frame Flow storage;
|
||
the frame remains unchanged. Display uses that exact previewed asset.
|
||
- **Schedule random photos** creates a device-bound Cron job, paused by default.
|
||
Each execution picks a random photo; ordinary random selection can repeat.
|
||
Editing the job supports other schedule types, fitting/profile via advanced JSON,
|
||
and the existing temporary override/restore options.
|
||
- Sources can be edited, disabled or removed. Remove referencing jobs before deletion.
|
||
|
||
Metadata is cached for 60 seconds, with explicit refresh. Each catalog is limited
|
||
to 10,000 supported images (not a complete inventory for larger folders).
|
||
Maximum selected image size is 20 MiB; WebDAV XML responses are limited to 2 MiB.
|
||
Preview imports use the existing local asset store; monitor disk space over time.
|
||
Unreadable/invalid images fail the operation without retrying frame writes.
|
||
|
||
## API
|
||
|
||
- `GET /api/v1/photo-sources`
|
||
- `PUT /api/v1/photo-sources/{id}` (admin)
|
||
- `DELETE /api/v1/photo-sources/{id}` (admin)
|
||
- `GET /api/v1/photo-sources/{id}/images?offset=0&limit=50&refresh=false`
|
||
|
||
Display or Scheduler content:
|
||
|
||
```json
|
||
{"type":"photo_source","source_id":"family","fit":"contain","profile":"color"}
|
||
```
|
||
|
||
Optional `item_id` selects an exact catalog item. Omit it for random selection.
|
||
The device upload endpoint accepts the same content with `display_now=false`
|
||
(default) to import/preview locally. `POST /api/v1/display` performs the usual
|
||
protected asynchronous device display. Schemas are in `/openapi.json` and `/docs`.
|
||
|
||
Native Aliyun Drive/Baidu Drive/Google Drive adapters are not implemented.
|
||
A compatible WebDAV gateway or an independently managed NAS sync folder can be used.
|
||
Frame Flow does not install a gateway or receive vendor API credentials automatically.
|
||
|
||
## Date-folder captions / 整理照片文字
|
||
|
||
Enable **Only photos in date-prefixed folders** and **Show folder caption below
|
||
the photo** when editing a source. Curated sources created by the UI increase the
|
||
catalog limit to 100,000 photos; other sources default to 10,000. The API exposes
|
||
`catalog_limit` explicitly. It remains a bounded catalog; reaching the limit is
|
||
shown in the UI.
|
||
|
||
Example folder name: `20261005 家庭出游@示例地点`.
|
||
The image contains the photo and three separate caption lines:
|
||
|
||
```text
|
||
家庭出游
|
||
2026-10-05
|
||
示例地点
|
||
```
|
||
|
||
Without `@`, the location line is omitted. Folder names support `YYYYMMDD`,
|
||
`YYYY-MM-DD`, `YYYY.MM.DD`, and `YYYY年MM月DD日`, with calendar-valid dates.
|
||
Subfolders inherit their nearest dated ancestor; image filenames and EXIF do not
|
||
replace this curated date. Synology `#recycle`, hidden folders, and `@eaDir` are
|
||
excluded. Extremely long captions shrink and then truncate visually; full values
|
||
remain available in catalog metadata.
|
||
|
||
Output is 1200×1600: a 1200×1280 photo area plus a white caption area. Original
|
||
files remain unchanged. Contain/cover/smart and color/grayscale profiles still apply.
|
||
A prepared source definition is in `deploy/organized-photos-source.json`; use it
|
||
with `PUT /api/v1/photo-sources/organized-photos` after mounting the library.
|
||
|
||
For Synology Photos shared libraries, `synology_previews: true` allows images over
|
||
20 MiB to use their existing `@eaDir/<filename>/SYNOPHOTO_THUMB_XL.jpg` preview.
|
||
This is a read-only fallback: original files and Photos indexes remain unchanged,
|
||
no thumbnails are generated on the NAS, and the ordinary size limit still applies
|
||
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.
|
||
|
||
## 照片方向 / Orientation
|
||
|
||
编辑照片数据源可选:横向、竖向、自动识别。自动根据目标设备尺寸和旋转方向计算画布;例如 1200×1600、rotation=270 输出 1600×1200 并选择横向。手动横向输出 1600×1200,手动竖向输出 1200×1600(以当前设备为例)。照片区和底部三行文字均随画布重排。按 EXIF 校正后的 width/height 分类,正方形不混入横向或竖向。预览、指定图片和定时随机选图使用同一筛选结果;空结果明确失败,不退回其他方向。照片不为匹配方向强制旋转或拉伸。
|
||
|
||
已有数据源 `orientation=null` 保持原来的全部方向,编辑器显示“不筛方向(原有设置)”;新建数据源默认自动。尺寸缓存持久化在 SQLite `photo_dimensions`,按路径、修改时间和大小失效。首次扫描读取本地图片尺寸和 EXIF,不批量生成全图库预览;后续刷新复用缓存。仅 NAS 本地源支持,WebDAV 不批量下载原图测尺寸。自动模式读取统一 Device Model,不依赖 KoKonna 私有字段;90°/270° 交换宽高。
|
||
|
||
Orientation options: landscape, portrait, auto. Auto follows the target device dimensions and rotation (90°/270° swap width and height). Manual direction controls both selection and rendering. Photo and caption layouts support landscape and portrait. Classification honors EXIF orientation; square photos are excluded from directional matches. Existing sources remain unfiltered (`null`) until changed. Dimensions persist in SQLite and refresh when the file changes. Local NAS sources only; no bulk original downloads from WebDAV. Empty matches fail closed. This controls selection, not photo rotation or stretching.
|
||
|
||
### Invalid photos and selection tracing
|
||
|
||
Local catalogs share the renderer's 25-million-pixel limit. Oversized originals
|
||
use an existing Synology XL preview when enabled and available; otherwise they
|
||
are excluded. Header-only catalog inspection does not guarantee full decoding.
|
||
Random rendering tries at most five distinct candidates before any device write,
|
||
skipping only invalid or unavailable images. Explicit selections never fall back.
|
||
Operation `result.photo_attempts` records source-relative item IDs, names, status
|
||
and stable error codes, including failures. This does not retry upload or switch.
|
||
|
||
### Local favorites, origins and minimal cloud storage
|
||
|
||
Stars are now stored on local assets in SQLite. The local library filters by
|
||
`origin` (`manual`, `task`, `external`, `template`, `unknown`) and `job_id`.
|
||
Task ID and name are recorded when an asset is generated; deleted tasks do not
|
||
rewrite that historical label. Older assets without reliable provenance remain
|
||
unknown. Multi-select supports star, unstar and deletion with per-item results.
|
||
Referenced assets are rejected rather than breaking pending operations/jobs.
|
||
|
||
Every new task upload is tracked by cloud ID, local asset ID and task ID. Each
|
||
task keeps its latest confirmed cloud image. Uploaded local favorites are kept
|
||
as cloud copies (one confirmed copy per local asset). New images must reach
|
||
COMPLETED with device `synced=true` before an older image can be removed. A
|
||
failed/unknown replacement leaves the previous image intact. Current, active
|
||
restore/override and playlist/explicit job references remain protected. Cleanup
|
||
checks at most once per minute and uses the existing queue, history, write guard
|
||
and UNKNOWN barrier. Deletion is never automatically retried after a failure or
|
||
unknown response. Superseded task-owned previous-image references are released
|
||
only once a replacement is confirmed; unrelated restore references are kept.
|
||
|
||
Starring a local image does not immediately upload it: KoKonna upload changes
|
||
the display. The star protects its cloud copy after a normal display upload.
|
||
Local originals remain available even if cloud cleanup removes the cloud copy.
|
||
NAS source originals are never deleted by these operations. Existing unmanaged
|
||
cloud images are not automatically swept; clearing them is an explicit action.
|
||
The previous `cleanup_unstarred` seven-day field remains accepted for API
|
||
compatibility but is superseded by the minimal-cloud policy in the scheduler.
|
||
|
||
Endpoints:
|
||
- `PUT /api/v1/assets/{asset_id}/star` with `{"starred": true}`.
|
||
- `POST /api/v1/assets/batch` with `asset_ids` (up to 100) and action
|
||
`star`, `unstar` or `delete`; per-item completion/failure results.
|
||
- `GET /api/v1/assets?origin=task&job_id=...&offset=0&limit=200`.
|
||
- Administrator `POST /api/v1/devices/{device_id}/images/clear` with
|
||
`{"confirm_current": true}` explicitly clears all listed cloud images,
|
||
including current and restore references. It returns queued operation IDs.
|
||
This is destructive, does not restore artwork, and stops at unknown writes.
|
||
|
||
The legacy cloud star route forwards to the linked local asset when available;
|
||
cloud star controls are removed from the UI. App/Synology favorites are separate.
|