Initial release
Build and Push Docker Image / build (push) Successful in 17s

This commit is contained in:
Stardream
2026-06-16 19:26:32 +10:00
commit e3bf77296c
142 changed files with 5481 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
'use strict';
async function cacheCoversAuto(){
const portable = await localApiAvailable();
const todo = getAllCollections().filter(c => c.status !== 'confirmed_deleted' && !c.cover_local_url);
if(!todo.length){ log('syncLog','封面自动缓存:全部已缓存,跳过。'); return; }
log('syncLog', `封面自动缓存:开始缓存 ${todo.length} 个未缓存封面${portable ? '到 Docker 文件卷' : '到浏览器 IndexedDB'}...`);
let ok=0, fail=0;
for(let i=0; i<todo.length; i++){
const c=todo[i]; const id=String(c.subject_id);
const url=coverUrl({...c, cover_local_url:'', cover_data_url:''}); if(!url){ fail++; continue; }
try{
if(portable){
const js=await localPostJSON('/api/cache-cover', {subject_id:id, url});
if(!js?.ok || !js.url) throw new Error(js?.error||'no url');
const row=state.collections[id];
if(row){ row.cover_local_url=js.url; row.cover_local_file=js.file||''; row.cover_cached_at=nowISO(); }
} else {
const res=await fetch(url,{mode:'cors',referrerPolicy:'no-referrer'}); if(!res.ok) throw new Error(res.statusText);
const blob=await res.blob(); const dataUrl=await blobToDataURL(blob); await idbSet('img:'+id,{dataUrl,updated_at:nowISO(),source:url});
}
ok++; if((i+1)%20===0){ log('syncLog',`封面自动缓存 ${i+1}/${todo.length}`); await saveState(); }
await sleep(portable ? 35 : 80);
}catch(e){ fail++; if(fail<=5) log('syncLog',`封面失败 ${id}${e.message||e}`); }
}
await saveState();
log('syncLog', `封面自动缓存完成:成功 ${ok},失败 ${fail}`);
}
async function cacheCoversCurrent(){
const arr = visibleCollections(); if(!arr.length){ toast('没有可缓存的封面'); return; }
const portable = await localApiAvailable();
openModal('syncModal'); $('syncLog').textContent='';
log('syncLog', portable ? `开始缓存当前筛选下 ${arr.length} 个封面到 Docker 文件卷...` : `未检测到 Web 服务,临时缓存到浏览器 IndexedDB:${arr.length} 个封面...`);
setProgress(0);
let ok=0, fail=0, i=0;
for(const c of arr){
i++; setProgress(i/arr.length*100); const id=String(c.subject_id);
if(c.cover_local_url || (!portable && await getCachedImage(id))) { ok++; continue; }
const url=coverUrl({...c, cover_local_url:'', cover_data_url:''}); if(!url){ fail++; continue; }
try{
if(portable){
const js = await localPostJSON('/api/cache-cover', {subject_id:id, url, title:c.title||''});
if(!js?.ok || !js.url) throw new Error(js?.error || '本地服务没有返回图片路径');
const row = state.collections[id];
if(row){ row.cover_local_url = js.url; row.cover_local_file = js.file || ''; row.cover_cached_at = nowISO(); }
} else {
const res=await fetch(url, {mode:'cors', referrerPolicy:'no-referrer'}); if(!res.ok) throw new Error(res.statusText); const blob=await res.blob(); const dataUrl=await blobToDataURL(blob); await idbSet('img:'+id, {dataUrl, updated_at:nowISO(), source:url});
}
ok++; if(i%10===0){ log('syncLog',`已处理 ${i}/${arr.length}`); await saveState(); }
await sleep(portable ? 35 : 80);
}catch(e){ fail++; if(fail <= 5) log('syncLog',`封面失败 ${id}${e.message || e}`); }
}
await saveState();
log('syncLog',`封面缓存完成:成功 ${ok},失败 ${fail}`); toast(`封面缓存完成:成功 ${ok},失败 ${fail}`); render();
}
function blobToDataURL(blob){ return new Promise((resolve,reject)=>{ const r=new FileReader(); r.onload=()=>resolve(r.result); r.onerror=()=>reject(r.error); r.readAsDataURL(blob); }); }
async function imageBlobToDataURLFromCache(c){
const offlineData = c.cover_data_url || '';
const localUrl = c.cover_local_url || '';
const remoteUrl = onlineCoverUrl(c);
// v0.20: 导出用的 base64 只在内存中的离线副本里生成,不写回 收藏数据.json。
// Web 运行界面优先使用服务端缓存封面或在线原图。
if(localUrl){
try{ const res = await fetch(localUrl, {cache:'no-store'}); if(res.ok) return await blobToDataURL(await res.blob()); }catch{}
}
try{ const cached = await idbGet('img:'+c.subject_id); if(cached?.dataUrl) return cached.dataUrl; }catch{}
if(offlineData) return offlineData;
// 没有本地缓存时,不主动在线拉远端图片进 HTML,避免导出完整 ZIP 时大量跨域下载卡住。
// 需要离线封面的条目,请先点“缓存当前封面到目录”。
return '';
}
async function collectCachedImageFiles(arr,onProgress){
const files = {};
let i = 0;
for(const c of arr){
i++;
if(onProgress) onProgress(i, arr.length, c);
if(!c.cover_local_url) { if(i % 40 === 0) await sleep(0); continue; }
const file = (c.cover_local_file || '').split(/[\\/]/).pop() || (`${c.subject_id}.jpg`);
try{ const res = await fetch(c.cover_local_url, {cache:'no-store'}); if(!res.ok) continue; files['images/' + file] = new Uint8Array(await res.arrayBuffer()); }
catch{}
if(i % 30 === 0) await sleep(0);
}
return files;
}