Move favorites to local library and keep confirmed task heads in cloud
This commit is contained in:
+37
-18
@@ -162,24 +162,43 @@ 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.
|
||||
|
||||
### Starred cloud photos and seven-day retention
|
||||
### Local favorites, origins and minimal cloud storage
|
||||
|
||||
Automation `cleanup_unstarred` is opt-in and defaults to false. It is valid only
|
||||
for random photo-source jobs. New uploads retain their confirmed cloud image ID
|
||||
and an expiry seven days after upload. Existing images are never backfilled.
|
||||
The cloud gallery's star button stores a Frame Flow star in SQLite; it does not
|
||||
modify Synology Photos favorites or synchronize with KoKonna app favorites.
|
||||
Starred images stay; unstarred overdue images are eligible for deletion from the
|
||||
KoKonna cloud gallery. NAS originals and local Frame Flow assets remain.
|
||||
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.
|
||||
|
||||
The scheduler checks hourly, queues at most 20 deletions per cycle, and rechecks
|
||||
stars and protection when executing. Current, previous/restore, override,
|
||||
playlist and explicit job image references are protected. Offline devices and
|
||||
write-disabled services skip cleanup. A failed or unknown deletion is retained
|
||||
for investigation and is not retried automatically. Unstarring an expired image
|
||||
makes it eligible at the next check. Removing a job does not cancel retention
|
||||
already assigned to its uploads; star these images to keep them.
|
||||
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.
|
||||
|
||||
API: `PUT /api/v1/devices/{device_id}/images/{image_id}/star` with
|
||||
`{"starred": true}`; scope `images:write`. Gallery responses add `starred` and
|
||||
`expires_at`. All cleanup uses the existing operation queue, history and driver.
|
||||
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.
|
||||
|
||||
@@ -1567,6 +1567,164 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/devices/{device_id}/images/clear": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Images"
|
||||
],
|
||||
"summary": "Clear Cloud",
|
||||
"operationId": "clear_cloud_api_v1_devices__device_id__images_clear_post",
|
||||
"security": [
|
||||
{
|
||||
"HTTPBearer": []
|
||||
},
|
||||
{
|
||||
"AccountSession": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "device_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Device Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Not Found"
|
||||
},
|
||||
"409": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Conflict"
|
||||
},
|
||||
"413": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Content Too Large"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Unprocessable Content"
|
||||
},
|
||||
"429": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Too Many Requests"
|
||||
},
|
||||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Internal Server Error"
|
||||
},
|
||||
"502": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Bad Gateway"
|
||||
},
|
||||
"503": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Service Unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/devices/{device_id}/images/{image_id}/file": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -2803,6 +2961,51 @@
|
||||
"default": "all",
|
||||
"title": "Source"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "origin",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"enum": [
|
||||
"all",
|
||||
"manual",
|
||||
"task",
|
||||
"external",
|
||||
"template",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string",
|
||||
"default": "all",
|
||||
"title": "Origin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "job_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Job Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "offset",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"default": 0,
|
||||
"title": "Offset"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -3228,6 +3431,313 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/assets/{asset_id}/star": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"Images"
|
||||
],
|
||||
"summary": "Star Asset",
|
||||
"operationId": "star_asset_api_v1_assets__asset_id__star_put",
|
||||
"security": [
|
||||
{
|
||||
"HTTPBearer": []
|
||||
},
|
||||
{
|
||||
"AccountSession": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "asset_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Asset Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ImageStar"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Asset"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Not Found"
|
||||
},
|
||||
"409": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Conflict"
|
||||
},
|
||||
"413": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Content Too Large"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Unprocessable Content"
|
||||
},
|
||||
"429": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Too Many Requests"
|
||||
},
|
||||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Internal Server Error"
|
||||
},
|
||||
"502": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Bad Gateway"
|
||||
},
|
||||
"503": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Service Unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/assets/batch": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Images"
|
||||
],
|
||||
"summary": "Batch Assets",
|
||||
"operationId": "batch_assets_api_v1_assets_batch_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssetBatch"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -9679,6 +10189,45 @@
|
||||
},
|
||||
"Asset": {
|
||||
"properties": {
|
||||
"starred": {
|
||||
"type": "boolean",
|
||||
"title": "Starred",
|
||||
"default": false
|
||||
},
|
||||
"origin": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"manual",
|
||||
"task",
|
||||
"external",
|
||||
"template",
|
||||
"unknown"
|
||||
],
|
||||
"title": "Origin",
|
||||
"default": "unknown"
|
||||
},
|
||||
"job_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Job Id"
|
||||
},
|
||||
"job_name": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Job Name"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -9714,6 +10263,35 @@
|
||||
],
|
||||
"title": "Asset"
|
||||
},
|
||||
"AssetBatch": {
|
||||
"properties": {
|
||||
"asset_ids": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"maxItems": 100,
|
||||
"minItems": 1,
|
||||
"title": "Asset Ids"
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"delete",
|
||||
"star",
|
||||
"unstar"
|
||||
],
|
||||
"title": "Action"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"asset_ids",
|
||||
"action"
|
||||
],
|
||||
"title": "AssetBatch"
|
||||
},
|
||||
"Body_preview_api_v1_preview_post": {
|
||||
"properties": {
|
||||
"file": {
|
||||
@@ -9973,6 +10551,18 @@
|
||||
],
|
||||
"title": "CurrentDisplay"
|
||||
},
|
||||
"DeleteRequest": {
|
||||
"properties": {
|
||||
"confirm_current": {
|
||||
"type": "boolean",
|
||||
"title": "Confirm Current",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"title": "DeleteRequest"
|
||||
},
|
||||
"Device": {
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
Reference in New Issue
Block a user