feat: show original NAS photo paths in local gallery cards
This commit is contained in:
@@ -14203,6 +14203,17 @@
|
||||
},
|
||||
"Asset": {
|
||||
"properties": {
|
||||
"source_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Source Path"
|
||||
},
|
||||
"starred": {
|
||||
"type": "boolean",
|
||||
"title": "Starred",
|
||||
@@ -16227,6 +16238,17 @@
|
||||
],
|
||||
"title": "Caption"
|
||||
},
|
||||
"source_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Source Path"
|
||||
},
|
||||
"provider_preview": {
|
||||
"type": "boolean",
|
||||
"title": "Provider Preview",
|
||||
@@ -16346,6 +16368,19 @@
|
||||
"minLength": 1,
|
||||
"title": "Location"
|
||||
},
|
||||
"nas_root_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"maxLength": 2048,
|
||||
"pattern": "^/"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Nas Root Path"
|
||||
},
|
||||
"recursive": {
|
||||
"type": "boolean",
|
||||
"title": "Recursive",
|
||||
@@ -16513,6 +16548,19 @@
|
||||
"minLength": 1,
|
||||
"title": "Location"
|
||||
},
|
||||
"nas_root_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"maxLength": 2048,
|
||||
"pattern": "^/"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Nas Root Path"
|
||||
},
|
||||
"recursive": {
|
||||
"type": "boolean",
|
||||
"title": "Recursive",
|
||||
|
||||
@@ -392,7 +392,24 @@ class DisplayService:
|
||||
"UPDATE operations SET result=? WHERE 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(
|
||||
E.INVALID_REQUEST, "No decodable photo found within 5 candidates"
|
||||
)
|
||||
|
||||
@@ -375,6 +375,7 @@ class Readiness(BaseModel):
|
||||
|
||||
|
||||
class Asset(BaseModel):
|
||||
source_path: str | None = None
|
||||
starred: bool = False
|
||||
origin: Literal["manual", "task", "external", "template", "unknown"] = "unknown"
|
||||
job_id: str | None = None
|
||||
|
||||
@@ -32,6 +32,7 @@ class PhotoSourceInput(StrictModel):
|
||||
"local", "synology-photos", "webdav", "google-drive", "alipan", "baidu"
|
||||
]
|
||||
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
|
||||
enabled: bool = True
|
||||
catalog_limit: int = Field(default=10000, ge=1, le=100000)
|
||||
@@ -62,6 +63,7 @@ class PhotoItem(StrictModel):
|
||||
id: str
|
||||
name: str
|
||||
caption: dict[str, str] | None = None
|
||||
source_path: str | None = None
|
||||
provider_preview: bool = False
|
||||
orientation: Literal["landscape", "portrait", "square"] | None = None
|
||||
width: int | None = None
|
||||
@@ -492,6 +494,13 @@ class PhotoSources:
|
||||
"id": str(row["id"]),
|
||||
"name": row["filename"],
|
||||
"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,
|
||||
"width": width,
|
||||
"height": height,
|
||||
|
||||
@@ -134,7 +134,13 @@ class Assets:
|
||||
if not row:
|
||||
raise FlowError(E.NOT_FOUND, "Asset not found", 404)
|
||||
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 {
|
||||
"source_path": json.loads(provenance["payload"]).get("source_path")
|
||||
if provenance
|
||||
else None,
|
||||
"starred": bool(meta and meta["starred"]),
|
||||
"origin": meta["origin"] if meta else "unknown",
|
||||
"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 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 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);
|
||||
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):''));
|
||||
|
||||
@@ -150,9 +150,9 @@ async function photoSourcesPanel(){
|
||||
section.querySelectorAll('[data-source-job]').forEach(b=>b.onclick=()=>sourceJobForm(b.dataset.sourceJob));
|
||||
}
|
||||
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>`);
|
||||
$('#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='';};
|
||||
$('#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');});
|
||||
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-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,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){
|
||||
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 .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}
|
||||
|
||||
.image-card h3.source-path{white-space:normal;overflow-wrap:anywhere}
|
||||
|
||||
@@ -883,6 +883,19 @@
|
||||
"minLength": 1,
|
||||
"title": "Location"
|
||||
},
|
||||
"nas_root_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"maxLength": 2048,
|
||||
"pattern": "^/"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Nas Root Path"
|
||||
},
|
||||
"recursive": {
|
||||
"type": "boolean",
|
||||
"title": "Recursive",
|
||||
@@ -1050,6 +1063,19 @@
|
||||
"minLength": 1,
|
||||
"title": "Location"
|
||||
},
|
||||
"nas_root_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"maxLength": 2048,
|
||||
"pattern": "^/"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Nas Root Path"
|
||||
},
|
||||
"recursive": {
|
||||
"type": "boolean",
|
||||
"title": "Recursive",
|
||||
@@ -1220,6 +1246,17 @@
|
||||
],
|
||||
"title": "Caption"
|
||||
},
|
||||
"source_path": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Source Path"
|
||||
},
|
||||
"provider_preview": {
|
||||
"type": "boolean",
|
||||
"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