feat: show original NAS photo paths in local gallery cards
This commit is contained in:
@@ -14203,6 +14203,17 @@
|
|||||||
},
|
},
|
||||||
"Asset": {
|
"Asset": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"source_path": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Source Path"
|
||||||
|
},
|
||||||
"starred": {
|
"starred": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"title": "Starred",
|
"title": "Starred",
|
||||||
@@ -16227,6 +16238,17 @@
|
|||||||
],
|
],
|
||||||
"title": "Caption"
|
"title": "Caption"
|
||||||
},
|
},
|
||||||
|
"source_path": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Source Path"
|
||||||
|
},
|
||||||
"provider_preview": {
|
"provider_preview": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"title": "Provider Preview",
|
"title": "Provider Preview",
|
||||||
@@ -16346,6 +16368,19 @@
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "Location"
|
"title": "Location"
|
||||||
},
|
},
|
||||||
|
"nas_root_path": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 2048,
|
||||||
|
"pattern": "^/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Nas Root Path"
|
||||||
|
},
|
||||||
"recursive": {
|
"recursive": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"title": "Recursive",
|
"title": "Recursive",
|
||||||
@@ -16513,6 +16548,19 @@
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "Location"
|
"title": "Location"
|
||||||
},
|
},
|
||||||
|
"nas_root_path": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 2048,
|
||||||
|
"pattern": "^/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Nas Root Path"
|
||||||
|
},
|
||||||
"recursive": {
|
"recursive": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"title": "Recursive",
|
"title": "Recursive",
|
||||||
|
|||||||
@@ -392,7 +392,24 @@ class DisplayService:
|
|||||||
"UPDATE operations SET result=? WHERE id=?",
|
"UPDATE operations SET result=? WHERE id=?",
|
||||||
(encode({"photo_attempts": attempts}), operation_id),
|
(encode({"photo_attempts": attempts}), operation_id),
|
||||||
)
|
)
|
||||||
return self.assets.put(rendered, name, "cache" if staging else "images")
|
asset = self.assets.put(
|
||||||
|
rendered, name, "cache" if staging else "images"
|
||||||
|
)
|
||||||
|
self.db.execute(
|
||||||
|
"INSERT OR REPLACE INTO asset_sources VALUES(?,?)",
|
||||||
|
(
|
||||||
|
asset["asset_id"],
|
||||||
|
encode(
|
||||||
|
{
|
||||||
|
"type": "photo_source",
|
||||||
|
"source_id": content.source_id,
|
||||||
|
"item_id": item["id"],
|
||||||
|
"source_path": item.get("source_path"),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return self.assets.info(asset["asset_id"])
|
||||||
raise FlowError(
|
raise FlowError(
|
||||||
E.INVALID_REQUEST, "No decodable photo found within 5 candidates"
|
E.INVALID_REQUEST, "No decodable photo found within 5 candidates"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -375,6 +375,7 @@ class Readiness(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Asset(BaseModel):
|
class Asset(BaseModel):
|
||||||
|
source_path: str | None = None
|
||||||
starred: bool = False
|
starred: bool = False
|
||||||
origin: Literal["manual", "task", "external", "template", "unknown"] = "unknown"
|
origin: Literal["manual", "task", "external", "template", "unknown"] = "unknown"
|
||||||
job_id: str | None = None
|
job_id: str | None = None
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class PhotoSourceInput(StrictModel):
|
|||||||
"local", "synology-photos", "webdav", "google-drive", "alipan", "baidu"
|
"local", "synology-photos", "webdav", "google-drive", "alipan", "baidu"
|
||||||
]
|
]
|
||||||
location: str = Field(min_length=1, max_length=2048)
|
location: str = Field(min_length=1, max_length=2048)
|
||||||
|
nas_root_path: str | None = Field(default=None, max_length=2048, pattern=r"^/")
|
||||||
recursive: bool = True
|
recursive: bool = True
|
||||||
enabled: bool = True
|
enabled: bool = True
|
||||||
catalog_limit: int = Field(default=10000, ge=1, le=100000)
|
catalog_limit: int = Field(default=10000, ge=1, le=100000)
|
||||||
@@ -62,6 +63,7 @@ class PhotoItem(StrictModel):
|
|||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
caption: dict[str, str] | None = None
|
caption: dict[str, str] | None = None
|
||||||
|
source_path: str | None = None
|
||||||
provider_preview: bool = False
|
provider_preview: bool = False
|
||||||
orientation: Literal["landscape", "portrait", "square"] | None = None
|
orientation: Literal["landscape", "portrait", "square"] | None = None
|
||||||
width: int | None = None
|
width: int | None = None
|
||||||
@@ -492,6 +494,13 @@ class PhotoSources:
|
|||||||
"id": str(row["id"]),
|
"id": str(row["id"]),
|
||||||
"name": row["filename"],
|
"name": row["filename"],
|
||||||
"provider_preview": True,
|
"provider_preview": True,
|
||||||
|
"source_path": (
|
||||||
|
source.nas_root_path.rstrip("/")
|
||||||
|
+ "/"
|
||||||
|
+ relative.lstrip("/")
|
||||||
|
if source.nas_root_path
|
||||||
|
else "/" + relative.lstrip("/")
|
||||||
|
),
|
||||||
"caption": caption if source.folder_caption else None,
|
"caption": caption if source.folder_caption else None,
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
|
|||||||
@@ -134,7 +134,13 @@ class Assets:
|
|||||||
if not row:
|
if not row:
|
||||||
raise FlowError(E.NOT_FOUND, "Asset not found", 404)
|
raise FlowError(E.NOT_FOUND, "Asset not found", 404)
|
||||||
meta = self.db.one("SELECT * FROM asset_metadata WHERE asset_id=?", (asset_id,))
|
meta = self.db.one("SELECT * FROM asset_metadata WHERE asset_id=?", (asset_id,))
|
||||||
|
provenance = self.db.one(
|
||||||
|
"SELECT payload FROM asset_sources WHERE asset_id=?", (asset_id,)
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
|
"source_path": json.loads(provenance["payload"]).get("source_path")
|
||||||
|
if provenance
|
||||||
|
else None,
|
||||||
"starred": bool(meta and meta["starred"]),
|
"starred": bool(meta and meta["starred"]),
|
||||||
"origin": meta["origin"] if meta else "unknown",
|
"origin": meta["origin"] if meta else "unknown",
|
||||||
"job_id": meta["job_id"] if meta else None,
|
"job_id": meta["job_id"] if meta else None,
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ const views={
|
|||||||
const destinations=galleryDestinations();if(!destinations.some(d=>d.id===state.tab))state.tab=destinations[0]?.id||'local';
|
const destinations=galleryDestinations();if(!destinations.some(d=>d.id===state.tab))state.tab=destinations[0]?.id||'local';
|
||||||
const key=JSON.stringify([state.device,state.tab,state.assetOffset||0,state.assetOrigin||'all',state.assetJob||'']);
|
const key=JSON.stringify([state.device,state.tab,state.assetOffset||0,state.assetOrigin||'all',state.assetJob||'']);
|
||||||
const cache=state.galleryCache||(state.galleryCache=new Map());const cached=cache.get(key);
|
const cache=state.galleryCache||(state.galleryCache=new Map());const cached=cache.get(key);
|
||||||
const render=(items,pending=false)=>{if(epoch!==state.epoch)return;state.gallery=items;$('#main').innerHTML=notice()+`<div class="section-heading"><div class="tabs gallery-tabs">${destinations.map(d=>`<button id="${esc(d.id)}-tab" data-gallery-destination="${esc(d.id)}" class="${state.tab===d.id?'active':''}">${esc(galleryDestinationName(d))}</button>`).join('')}</div><div class="actions">${buttons('gallery-refresh','refresh')}${state.tab==='cloud'?buttons('show-name','displayByName'):''}${buttons('gallery-upload','upload',true)}<span id="gallery-list-status" class="hint" role="status" ${pending?'':'hidden'}>${pending?t('galleryListLoading'):''}</span></div></div>${state.tab==='local'?`<div class="notice">${t('uploadNotice')}</div>`:''}${`<div class="gallery" aria-busy="${pending}">${items.map((x,i)=>`<article class="card image-card"><button class="image-preview" data-original="${i}" aria-label="${t('original')} · ${esc(x.name)}"><img class="thumb" alt="${esc(x.name)}" data-src="${esc(x.url+(state.tab==='cloud'?'?thumbnail=true':''))}"></button><h3 title="${esc(x.name)}">${esc(x.name)}</h3><small>${state.tab==='cloud'?'#'+x.image_id:t('local')} · ${esc(fmt(x.created_at))}</small><div class="actions"><button class="primary" data-show="${i}" ${x.current?'disabled':''}>${t(x.current?'current':'display')}</button><button class="danger" data-delete="${i}">${t('delete')}</button></div></article>`).join('')}</div>`}${!pending&&!items.length?`<div class="card empty"><strong>${t('noImages')}</strong>${t('emptyHint')}</div>`:''}`;bind('cloud-tab',()=>{state.tab='cloud';return navigate();});bind('local-tab',()=>{state.tab='local';return navigate();});bind('gallery-refresh',()=>navigate());bind('gallery-upload',()=>navigate('upload'));bind('show-name',()=>{showModal(`<h2>${t('displayByName')}</h2><div class="field"><label for="image-name">${t('name')}</label><input id="image-name" maxlength="120"></div><div class="modal-actions">${buttons('modal-cancel','cancel')}${buttons('show-name-confirm','display',true)}</div>`);bind('show-name-confirm',async()=>{const name=$('#image-name').value;$('#modal').close();displayDialog({type:'image',image_name:name});});});enhanceGallery();guardDeviceButtons();hydrateImages();document.querySelectorAll('[data-show]').forEach(b=>b.onclick=busy(()=>{const x=items[b.dataset.show];if(x.current)return;return displayDialog(state.tab==='cloud'?{type:'image',image_id:x.image_id}:{type:'image',asset_id:x.asset_id});}));document.querySelectorAll('[data-original]').forEach(b=>b.onclick=busy(async()=>{const x=items[b.dataset.original];await openImageViewer(x.url,x.name);}));document.querySelectorAll('[data-delete]').forEach(b=>b.onclick=()=>{const x=items[b.dataset.delete];confirmDialog(state.tab==='local'?t('localDeleteConfirm'):x.current?t('deleteCurrent'):t('cloudDeleteConfirm'),async()=>{if(state.tab==='local'){await api('/api/v1/assets/'+x.asset_id,{method:'DELETE'});}else{const result=await api(`/api/v1/devices/${state.device}/images/${x.image_id}?confirm_current=${x.current}`,{method:'DELETE',headers:{'Idempotency-Key':'web-'+clientUUID()}});await waitOperation(result.operation_id);}await navigate();});});};
|
const render=(items,pending=false)=>{if(epoch!==state.epoch)return;state.gallery=items;$('#main').innerHTML=notice()+`<div class="section-heading"><div class="tabs gallery-tabs">${destinations.map(d=>`<button id="${esc(d.id)}-tab" data-gallery-destination="${esc(d.id)}" class="${state.tab===d.id?'active':''}">${esc(galleryDestinationName(d))}</button>`).join('')}</div><div class="actions">${buttons('gallery-refresh','refresh')}${state.tab==='cloud'?buttons('show-name','displayByName'):''}${buttons('gallery-upload','upload',true)}<span id="gallery-list-status" class="hint" role="status" ${pending?'':'hidden'}>${pending?t('galleryListLoading'):''}</span></div></div>${state.tab==='local'?`<div class="notice">${t('uploadNotice')}</div>`:''}${`<div class="gallery" aria-busy="${pending}">${items.map((x,i)=>`<article class="card image-card"><button class="image-preview" data-original="${i}" aria-label="${t('original')} · ${esc(x.name)}"><img class="thumb" alt="${esc(x.name)}" data-src="${esc(x.url+(state.tab==='cloud'?'?thumbnail=true':''))}"></button><h3 class="${state.tab==='local'?'source-path':''}" title="${esc(x.source_path||x.name)}">${esc(state.tab==='local'?(x.source_path||x.name):x.name)}</h3><small>${state.tab==='cloud'?'#'+x.image_id:t('local')} · ${esc(fmt(x.created_at))}</small><div class="actions"><button class="primary" data-show="${i}" ${x.current?'disabled':''}>${t(x.current?'current':'display')}</button><button class="danger" data-delete="${i}">${t('delete')}</button></div></article>`).join('')}</div>`}${!pending&&!items.length?`<div class="card empty"><strong>${t('noImages')}</strong>${t('emptyHint')}</div>`:''}`;bind('cloud-tab',()=>{state.tab='cloud';return navigate();});bind('local-tab',()=>{state.tab='local';return navigate();});bind('gallery-refresh',()=>navigate());bind('gallery-upload',()=>navigate('upload'));bind('show-name',()=>{showModal(`<h2>${t('displayByName')}</h2><div class="field"><label for="image-name">${t('name')}</label><input id="image-name" maxlength="120"></div><div class="modal-actions">${buttons('modal-cancel','cancel')}${buttons('show-name-confirm','display',true)}</div>`);bind('show-name-confirm',async()=>{const name=$('#image-name').value;$('#modal').close();displayDialog({type:'image',image_name:name});});});enhanceGallery();guardDeviceButtons();hydrateImages();document.querySelectorAll('[data-show]').forEach(b=>b.onclick=busy(()=>{const x=items[b.dataset.show];if(x.current)return;return displayDialog(state.tab==='cloud'?{type:'image',image_id:x.image_id}:{type:'image',asset_id:x.asset_id});}));document.querySelectorAll('[data-original]').forEach(b=>b.onclick=busy(async()=>{const x=items[b.dataset.original];await openImageViewer(x.url,x.name);}));document.querySelectorAll('[data-delete]').forEach(b=>b.onclick=()=>{const x=items[b.dataset.delete];confirmDialog(state.tab==='local'?t('localDeleteConfirm'):x.current?t('deleteCurrent'):t('cloudDeleteConfirm'),async()=>{if(state.tab==='local'){await api('/api/v1/assets/'+x.asset_id,{method:'DELETE'});}else{const result=await api(`/api/v1/devices/${state.device}/images/${x.image_id}?confirm_current=${x.current}`,{method:'DELETE',headers:{'Idempotency-Key':'web-'+clientUUID()}});await waitOperation(result.operation_id);}await navigate();});});};
|
||||||
render(cached||[],!cached);
|
render(cached||[],!cached);
|
||||||
try{
|
try{
|
||||||
const items=state.tab==='cloud'?await api('/api/v1/devices/'+state.device+'/images'):await api('/api/v1/assets?limit=200&offset='+(state.assetOffset||0)+'&origin='+encodeURIComponent(state.assetOrigin||'all')+(state.assetJob?'&job_id='+encodeURIComponent(state.assetJob):''));
|
const items=state.tab==='cloud'?await api('/api/v1/devices/'+state.device+'/images'):await api('/api/v1/assets?limit=200&offset='+(state.assetOffset||0)+'&origin='+encodeURIComponent(state.assetOrigin||'all')+(state.assetJob?'&job_id='+encodeURIComponent(state.assetJob):''));
|
||||||
|
|||||||
@@ -150,9 +150,9 @@ async function photoSourcesPanel(){
|
|||||||
section.querySelectorAll('[data-source-job]').forEach(b=>b.onclick=()=>sourceJobForm(b.dataset.sourceJob));
|
section.querySelectorAll('[data-source-job]').forEach(b=>b.onclick=()=>sourceJobForm(b.dataset.sourceJob));
|
||||||
}
|
}
|
||||||
async function photoSourceForm(source=null){
|
async function photoSourceForm(source=null){
|
||||||
showModal(`<h2>${t(source?'edit':'addSource')}</h2><p class="notice">${t('sourceIndependentHelp')}</p><form id="photo-source-form"><input id="source-id" type="hidden" value="${esc(source?.id||'photos-'+clientUUID())}"><div class="field"><label for="source-name">${t('name')}</label><input id="source-name" required maxlength="120" value="${esc(source?.name||'')}"></div><div class="field" ${!source||source.kind==='synology-photos'?'hidden':''}><label for="source-kind">${t('sourceKind')}</label><select id="source-kind"><option value="synology-photos">${t('synologyAPISource')}</option><option value="webdav">WebDAV</option><option value="google-drive">Google Drive</option><option value="alipan">阿里云盘 / AliPan</option><option value="baidu">百度网盘 / Baidu</option></select></div><div class="field"><label for="source-location">${t('sourceLocation')}</label><input id="source-location" required maxlength="2048" value="${esc(source?.location||'.')}"><p class="hint">${t('sourceLocationHint')}</p></div><div class="field"><label for="source-credential">${t('sourceCredential')}</label><input id="source-credential" pattern="FRAME_FLOW_SOURCE_[A-Z0-9_]+" value="${esc(source?.credential_env||'')}" placeholder="FRAME_FLOW_SOURCE_CLOUD"><p class="hint">${t('sourceCredentialHint')}</p></div><div class="field"><label for="source-orientation">${t('photoOrientation')}</label><select id="source-orientation">${source&&!source.orientation?`<option value="">${t('orientationLegacy')}</option>`:''}<option value="auto">${t('orientationAuto')}</option><option value="landscape">${t('orientationLandscape')}</option><option value="portrait">${t('orientationPortrait')}</option></select><p class="hint">${t('orientationHint')}</p></div><div class="field"><label class="check-row"><input id="source-organized" type="checkbox" ${source?.organized_only?'checked':''}>${t('organizedPhotos')}</label><label class="check-row"><input id="source-caption" type="checkbox" ${!source||source.folder_caption?'checked':''}>${t('folderCaption')}</label><label hidden><input id="source-synology-previews" type="checkbox" disabled></label><p class="hint">${t('folderCaptionHint')}</p></div><div class="field"><label class="check-row"><input id="source-people-filter" type="checkbox" ${source?.people_filter?'checked':''}>${t('peopleFilter')}</label><p class="hint">${t('peopleHint')}</p><button id="source-people-load" type="button">${t('choosePeople')}</button><div id="source-people-panel"><p class="hint">${t('peopleSelected')}${(source?.person_ids||[]).length}</p></div><label for="source-person-match">${t('personMatch')}</label><select id="source-person-match"><option value="any">${t('personAny')}</option><option value="all">${t('personAll')}</option></select></div><div class="field"><label class="check-row"><input id="source-recursive" type="checkbox" ${source?.recursive===false?'':'checked'}>${t('sourceRecursive')}</label><label class="check-row"><input id="source-enabled" type="checkbox" ${source?.enabled===false?'':'checked'}>${t('enabled')}</label></div><div class="modal-actions">${buttons('modal-cancel','cancel')}<button type="submit" class="primary">${t('save')}</button></div></form>`);
|
showModal(`<h2>${t(source?'edit':'addSource')}</h2><p class="notice">${t('sourceIndependentHelp')}</p><form id="photo-source-form"><input id="source-id" type="hidden" value="${esc(source?.id||'photos-'+clientUUID())}"><div class="field"><label for="source-name">${t('name')}</label><input id="source-name" required maxlength="120" value="${esc(source?.name||'')}"></div><div class="field" ${!source||source.kind==='synology-photos'?'hidden':''}><label for="source-kind">${t('sourceKind')}</label><select id="source-kind"><option value="synology-photos">${t('synologyAPISource')}</option><option value="webdav">WebDAV</option><option value="google-drive">Google Drive</option><option value="alipan">阿里云盘 / AliPan</option><option value="baidu">百度网盘 / Baidu</option></select></div><div class="field"><label for="source-location">${t('sourceLocation')}</label><input id="source-location" required maxlength="2048" value="${esc(source?.location||'.')}"><p class="hint">${t('sourceLocationHint')}</p></div><div class="field" id="source-nas-root-field"><label for="source-nas-root">${tr('NAS 原照片根目录(仅用于显示路径)','NAS original root (display only)')}</label><input id="source-nas-root" pattern="/.*" maxlength="2048" placeholder="/volume1/photo" value="${esc(source?.nas_root_path||'')}"><p class="hint">${tr('共享空间通常为 /volume1/photo,请填写实际 NAS 根目录;留空时显示 Photos 空间内路径。','Enter the actual NAS root; shared space is commonly /volume1/photo. Blank shows the path within Photos.')}</p></div><div class="field"><label for="source-credential">${t('sourceCredential')}</label><input id="source-credential" pattern="FRAME_FLOW_SOURCE_[A-Z0-9_]+" value="${esc(source?.credential_env||'')}" placeholder="FRAME_FLOW_SOURCE_CLOUD"><p class="hint">${t('sourceCredentialHint')}</p></div><div class="field"><label for="source-orientation">${t('photoOrientation')}</label><select id="source-orientation">${source&&!source.orientation?`<option value="">${t('orientationLegacy')}</option>`:''}<option value="auto">${t('orientationAuto')}</option><option value="landscape">${t('orientationLandscape')}</option><option value="portrait">${t('orientationPortrait')}</option></select><p class="hint">${t('orientationHint')}</p></div><div class="field"><label class="check-row"><input id="source-organized" type="checkbox" ${source?.organized_only?'checked':''}>${t('organizedPhotos')}</label><label class="check-row"><input id="source-caption" type="checkbox" ${!source||source.folder_caption?'checked':''}>${t('folderCaption')}</label><label hidden><input id="source-synology-previews" type="checkbox" disabled></label><p class="hint">${t('folderCaptionHint')}</p></div><div class="field"><label class="check-row"><input id="source-people-filter" type="checkbox" ${source?.people_filter?'checked':''}>${t('peopleFilter')}</label><p class="hint">${t('peopleHint')}</p><button id="source-people-load" type="button">${t('choosePeople')}</button><div id="source-people-panel"><p class="hint">${t('peopleSelected')}${(source?.person_ids||[]).length}</p></div><label for="source-person-match">${t('personMatch')}</label><select id="source-person-match"><option value="any">${t('personAny')}</option><option value="all">${t('personAll')}</option></select></div><div class="field"><label class="check-row"><input id="source-recursive" type="checkbox" ${source?.recursive===false?'':'checked'}>${t('sourceRecursive')}</label><label class="check-row"><input id="source-enabled" type="checkbox" ${source?.enabled===false?'':'checked'}>${t('enabled')}</label></div><div class="modal-actions">${buttons('modal-cancel','cancel')}<button type="submit" class="primary">${t('save')}</button></div></form>`);
|
||||||
$('#source-orientation').value=source?(source.orientation||''):'auto';const selectedPeople=new Set(source?.person_ids||[]);$('#source-person-match').value=source?.person_match||'any';$('#source-people-load').onclick=busy(async()=>{await photosPeoplePicker(selectedPeople);});$('#source-kind').value=source?.kind||'synology-photos';const update=()=>{const photosAPI=$('#source-kind').value==='synology-photos';$('#source-location').labels[0].textContent=photosAPI?tr('照片目录','Photo folder'):t('sourceLocation');$('#source-location').nextElementSibling.textContent=photosAPI?tr('填写所选空间内的目录路径,. 表示整个空间。按需获取 XL 预览图,不下载原图。','Folder path within the selected space; . means the whole space. Downloads XL previews on demand, not originals.'):t('sourceLocationHint');const dav=!['local','synology-photos'].includes($('#source-kind').value);$('#source-orientation').disabled=dav;$('#source-credential').disabled=!dav;$('#source-credential').closest('.field').hidden=$('#source-kind').value!=='webdav';const oauth=$('#source-oauth');if(oauth)oauth.closest('.field').hidden=!['google-drive','alipan','baidu'].includes($('#source-kind').value);const quality=$('#source-local-quality');if(quality)quality.hidden=dav;$('#source-recursive').disabled=$('#source-kind').value==='webdav';$('#source-synology-previews').disabled=$('#source-kind').value!=='local';$('#source-organized').disabled=dav;$('#source-caption').disabled=dav;$('#source-people-filter').disabled=dav;$('#source-people-load').disabled=dav;};addPhotoQualityFields(source);$('#source-location').closest('.field').before($('#source-people-space').closest('.field'));update();$('#source-kind').onchange=update;$('#source-people-space').onchange=()=>{selectedPeople.clear();$('#source-people-panel').innerHTML='';};
|
$('#source-orientation').value=source?(source.orientation||''):'auto';const selectedPeople=new Set(source?.person_ids||[]);$('#source-person-match').value=source?.person_match||'any';$('#source-people-load').onclick=busy(async()=>{await photosPeoplePicker(selectedPeople);});$('#source-kind').value=source?.kind||'synology-photos';const update=()=>{const photosAPI=$('#source-kind').value==='synology-photos';$('#source-nas-root-field').hidden=!photosAPI;$('#source-location').labels[0].textContent=photosAPI?tr('照片目录','Photo folder'):t('sourceLocation');$('#source-location').nextElementSibling.textContent=photosAPI?tr('填写所选空间内的目录路径,. 表示整个空间。按需获取 XL 预览图,不下载原图。','Folder path within the selected space; . means the whole space. Downloads XL previews on demand, not originals.'):t('sourceLocationHint');const dav=!['local','synology-photos'].includes($('#source-kind').value);$('#source-orientation').disabled=dav;$('#source-credential').disabled=!dav;$('#source-credential').closest('.field').hidden=$('#source-kind').value!=='webdav';const oauth=$('#source-oauth');if(oauth)oauth.closest('.field').hidden=!['google-drive','alipan','baidu'].includes($('#source-kind').value);const quality=$('#source-local-quality');if(quality)quality.hidden=dav;$('#source-recursive').disabled=$('#source-kind').value==='webdav';$('#source-synology-previews').disabled=$('#source-kind').value!=='local';$('#source-organized').disabled=dav;$('#source-caption').disabled=dav;$('#source-people-filter').disabled=dav;$('#source-people-load').disabled=dav;};addPhotoQualityFields(source);$('#source-location').closest('.field').before($('#source-people-space').closest('.field'));update();$('#source-kind').onchange=update;$('#source-people-space').onchange=()=>{selectedPeople.clear();$('#source-people-panel').innerHTML='';};
|
||||||
$('#photo-source-form').onsubmit=busy(async e=>{e.preventDefault();const oauth=$('#source-oauth').value;const sourceId=$('#source-id').value;const native=['google-drive','alipan','baidu'].includes($('#source-kind').value);if(oauth&&native)JSON.parse(oauth);const dav=!['local','synology-photos'].includes($('#source-kind').value);await api('/api/v1/photo-sources/'+$('#source-id').value,{method:'PUT',body:{name:$('#source-name').value,kind:$('#source-kind').value,location:$('#source-location').value,recursive:$('#source-kind').value!=='webdav'&&$('#source-recursive').checked,synology_previews:false,catalog_limit:$('#source-organized').checked?100000:(source?.catalog_limit||10000),organized_only:!dav&&$('#source-organized').checked,folder_caption:!dav&&$('#source-caption').checked,orientation:dav?null:($('#source-orientation').value||null),people_filter:!dav&&$('#source-people-filter').checked,person_ids:[...selectedPeople],person_match:$('#source-person-match').value,people_space:$('#source-people-space').value,minimum_short_edge:Number($('#source-min-short').value),minimum_long_edge:Number($('#source-min-long').value),minimum_sharpness:Number($('#source-sharpness').value),minimum_brightness:Number($('#source-bright-min').value),maximum_brightness:Number($('#source-bright-max').value),avoid_duplicates:$('#source-dedup').checked,enabled:$('#source-enabled').checked,credential_env:dav?($('#source-credential').value||null):null}});if(oauth&&native){$('#source-oauth').value='';await api('/api/v1/photo-sources/'+sourceId+'/credentials',{method:'PUT',body:{key:oauth}});}$('#modal').close();await navigate('photo-sources');});
|
$('#photo-source-form').onsubmit=busy(async e=>{e.preventDefault();const oauth=$('#source-oauth').value;const sourceId=$('#source-id').value;const native=['google-drive','alipan','baidu'].includes($('#source-kind').value);if(oauth&&native)JSON.parse(oauth);const dav=!['local','synology-photos'].includes($('#source-kind').value);await api('/api/v1/photo-sources/'+$('#source-id').value,{method:'PUT',body:{name:$('#source-name').value,kind:$('#source-kind').value,location:$('#source-location').value,nas_root_path:$('#source-nas-root').value||null,recursive:$('#source-kind').value!=='webdav'&&$('#source-recursive').checked,synology_previews:false,catalog_limit:$('#source-organized').checked?100000:(source?.catalog_limit||10000),organized_only:!dav&&$('#source-organized').checked,folder_caption:!dav&&$('#source-caption').checked,orientation:dav?null:($('#source-orientation').value||null),people_filter:!dav&&$('#source-people-filter').checked,person_ids:[...selectedPeople],person_match:$('#source-person-match').value,people_space:$('#source-people-space').value,minimum_short_edge:Number($('#source-min-short').value),minimum_long_edge:Number($('#source-min-long').value),minimum_sharpness:Number($('#source-sharpness').value),minimum_brightness:Number($('#source-bright-min').value),maximum_brightness:Number($('#source-bright-max').value),avoid_duplicates:$('#source-dedup').checked,enabled:$('#source-enabled').checked,credential_env:dav?($('#source-credential').value||null):null}});if(oauth&&native){$('#source-oauth').value='';await api('/api/v1/photo-sources/'+sourceId+'/credentials',{method:'PUT',body:{key:oauth}});}$('#modal').close();await navigate('photo-sources');});
|
||||||
}
|
}
|
||||||
async function browseSource(id,offset=0,refresh=false){
|
async function browseSource(id,offset=0,refresh=false){
|
||||||
const result=await api('/api/v1/photo-sources/'+id+'/images?device_id='+encodeURIComponent(state.device)+'&offset='+offset+'&limit=50&refresh='+refresh);const box=$('#source-browser');if(!box)return;
|
const result=await api('/api/v1/photo-sources/'+id+'/images?device_id='+encodeURIComponent(state.device)+'&offset='+offset+'&limit=50&refresh='+refresh);const box=$('#source-browser');if(!box)return;
|
||||||
|
|||||||
@@ -225,3 +225,5 @@ form#mqtt-config{grid-template-columns:minmax(160px,1fr) 110px minmax(180px,1.5f
|
|||||||
.automation-table td:is(:nth-child(2),:nth-child(5),:nth-child(6)){white-space:nowrap}
|
.automation-table td:is(:nth-child(2),:nth-child(5),:nth-child(6)){white-space:nowrap}
|
||||||
.automation-table .row-actions{flex-wrap:nowrap}
|
.automation-table .row-actions{flex-wrap:nowrap}
|
||||||
.history-table td:is(:nth-child(3),:nth-child(4),:nth-child(5)),.history-table .history-actions{vertical-align:middle}
|
.history-table td:is(:nth-child(3),:nth-child(4),:nth-child(5)),.history-table .history-actions{vertical-align:middle}
|
||||||
|
|
||||||
|
.image-card h3.source-path{white-space:normal;overflow-wrap:anywhere}
|
||||||
|
|||||||
@@ -883,6 +883,19 @@
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "Location"
|
"title": "Location"
|
||||||
},
|
},
|
||||||
|
"nas_root_path": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 2048,
|
||||||
|
"pattern": "^/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Nas Root Path"
|
||||||
|
},
|
||||||
"recursive": {
|
"recursive": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"title": "Recursive",
|
"title": "Recursive",
|
||||||
@@ -1050,6 +1063,19 @@
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"title": "Location"
|
"title": "Location"
|
||||||
},
|
},
|
||||||
|
"nas_root_path": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 2048,
|
||||||
|
"pattern": "^/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Nas Root Path"
|
||||||
|
},
|
||||||
"recursive": {
|
"recursive": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"title": "Recursive",
|
"title": "Recursive",
|
||||||
@@ -1220,6 +1246,17 @@
|
|||||||
],
|
],
|
||||||
"title": "Caption"
|
"title": "Caption"
|
||||||
},
|
},
|
||||||
|
"source_path": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Source Path"
|
||||||
|
},
|
||||||
"provider_preview": {
|
"provider_preview": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"title": "Provider Preview",
|
"title": "Provider Preview",
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import io
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from pigway_frame_flow.models import PhotoSourceContent
|
||||||
|
from pigway_frame_flow.providers.photo_sources import PhotoSourceInput
|
||||||
|
|
||||||
|
|
||||||
|
def test_photo_path_survives_render_and_asset_listing(client, service, monkeypatch):
|
||||||
|
photos = service.photo_sources
|
||||||
|
photos.save(
|
||||||
|
"family",
|
||||||
|
PhotoSourceInput(
|
||||||
|
name="Family",
|
||||||
|
kind="synology-photos",
|
||||||
|
location=".",
|
||||||
|
nas_root_path="/volume1/photo",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(photos.synology, "sid", lambda: "fixture")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
photos.synology,
|
||||||
|
"items",
|
||||||
|
lambda *a, **kw: [
|
||||||
|
{
|
||||||
|
"id": 42,
|
||||||
|
"filename": "original.PNG",
|
||||||
|
"additional": {
|
||||||
|
"folder": "/家庭/20190130",
|
||||||
|
"resolution": {"width": 1600, "height": 1200},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
out = io.BytesIO()
|
||||||
|
Image.new("RGB", (1600, 1200)).save(out, "PNG")
|
||||||
|
monkeypatch.setattr(photos.synology, "download", lambda *a, **kw: out.getvalue())
|
||||||
|
assert (
|
||||||
|
photos.catalog("family")[0]["source_path"]
|
||||||
|
== "/volume1/photo/家庭/20190130/original.PNG"
|
||||||
|
)
|
||||||
|
asset = service.content_asset(
|
||||||
|
PhotoSourceContent(type="photo_source", source_id="family", item_id="42")
|
||||||
|
)
|
||||||
|
assert asset["source_path"] == "/volume1/photo/家庭/20190130/original.PNG"
|
||||||
|
assert client.get("/api/v1/assets").json()[0]["source_path"] == asset["source_path"]
|
||||||
|
assert (
|
||||||
|
client.get("/api/v1/assets/" + asset["asset_id"] + "/source").json()["source"][
|
||||||
|
"item_id"
|
||||||
|
]
|
||||||
|
== "42"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_photo_path_is_not_invented(service):
|
||||||
|
out = io.BytesIO()
|
||||||
|
Image.new("RGB", (20, 20)).save(out, "JPEG")
|
||||||
|
asset = service.assets.put(out.getvalue(), "manual.jpg")
|
||||||
|
assert asset["source_path"] is None
|
||||||
Reference in New Issue
Block a user