fix: deliver JPEG (not huge PNG) to Picdrop, clean filenames, tmp cleanup
- Picdrop is a photo-proofing tool that expects handy JPGs; non-alpha results are now delivered as JPEG q92 (4:4:4) instead of 12-32 MB PNGs. Cutouts with alpha stay PNG. Fixes images not showing in POSTER LEA. - buildResultFilename strips doubled dates/UUIDs/hex to avoid messy names. - picdrop-ls gains action:cleanup_tmp; Admin 'Reste aufräumen' button removes leftover .tmp- files from the default gallery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
@@ -101,6 +101,12 @@ export default function AdminApp() {
|
|||||||
const r = await fetch('/api/admin/picdrop-ls', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }) }).then((x) => x.json());
|
const r = await fetch('/api/admin/picdrop-ls', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }) }).then((x) => x.json());
|
||||||
setPdList(r); notify(r.error ? r.error : `${(r.entries || []).length} Einträge in ${r.path}`);
|
setPdList(r); notify(r.error ? r.error : `${(r.entries || []).length} Einträge in ${r.path}`);
|
||||||
};
|
};
|
||||||
|
const cleanupPicdrop = async () => {
|
||||||
|
const gallery = s.picdrop_default_gallery || 'POSTER LEA';
|
||||||
|
notify('Räume Reste auf …');
|
||||||
|
const r = await fetch('/api/admin/picdrop-ls', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'cleanup_tmp', gallery }) }).then((x) => x.json());
|
||||||
|
notify(r.error ? r.error : `${r.cleaned ?? 0} Reste in „${gallery}" entfernt.`);
|
||||||
|
};
|
||||||
const testNas = async () => {
|
const testNas = async () => {
|
||||||
notify('Teste NAS …');
|
notify('Teste NAS …');
|
||||||
const r = await fetch('/api/admin/test-nas', { method: 'POST' }).then((x) => x.json());
|
const r = await fetch('/api/admin/test-nas', { method: 'POST' }).then((x) => x.json());
|
||||||
@@ -204,6 +210,7 @@ export default function AdminApp() {
|
|||||||
<section className="karte">
|
<section className="karte">
|
||||||
<div className="kopfzeile"><span className="mono-label">Picdrop</span>
|
<div className="kopfzeile"><span className="mono-label">Picdrop</span>
|
||||||
<div className="reihe"><button className="mini" onClick={() => lsPicdrop()}>Ordner anzeigen</button>
|
<div className="reihe"><button className="mini" onClick={() => lsPicdrop()}>Ordner anzeigen</button>
|
||||||
|
<button className="mini" onClick={cleanupPicdrop}>Reste aufräumen</button>
|
||||||
<button className="mini" onClick={testPicdrop}>Verbindung testen</button></div></div>
|
<button className="mini" onClick={testPicdrop}>Verbindung testen</button></div></div>
|
||||||
<div className="steuer">
|
<div className="steuer">
|
||||||
<div className="zwei">
|
<div className="zwei">
|
||||||
|
|||||||
+29
-4
@@ -45,9 +45,33 @@ async function sidecarForKey(key: string): Promise<boolean> {
|
|||||||
return !!t?.metadata_sidecar;
|
return !!t?.metadata_sidecar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Picdrop ist ein Foto-Proofing-Tool und erwartet handliche JPGs, keine 30-MB-PNGs.
|
||||||
|
* Ergebnisse ohne Transparenz werden darum als JPEG (q92) ausgeliefert; freigestellte
|
||||||
|
* Motive (Sticker mit Alpha) bleiben PNG. Rückgabe: umgewandelter Puffer + passender Name. */
|
||||||
|
async function deliverableBuffer(
|
||||||
|
buf: Buffer, filename: string, hasAlpha: boolean,
|
||||||
|
): Promise<{ buf: Buffer; name: string }> {
|
||||||
|
if (hasAlpha) return { buf, name: filename };
|
||||||
|
try {
|
||||||
|
const sharp = (await import('sharp')).default;
|
||||||
|
const meta = await sharp(buf, { failOn: 'none' }).metadata();
|
||||||
|
const dpi = meta.density && meta.density > 0 ? meta.density : 300;
|
||||||
|
const jpg = await sharp(buf, { failOn: 'none' })
|
||||||
|
.flatten({ background: '#ffffff' })
|
||||||
|
.withMetadata({ density: dpi })
|
||||||
|
.jpeg({ quality: 92, mozjpeg: true, chromaSubsampling: '4:4:4' })
|
||||||
|
.toBuffer();
|
||||||
|
const name = filename.replace(/\.(png|webp|tiff?)$/i, '.jpg');
|
||||||
|
return { buf: jpg, name: /\.jpe?g$/i.test(name) ? name : name + '.jpg' };
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[delivery] JPEG-Wandlung fehlgeschlagen, sende Original', e);
|
||||||
|
return { buf, name: filename };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Liefert ein fertiges Item an die passende Picdrop-Galerie aus. */
|
/** Liefert ein fertiges Item an die passende Picdrop-Galerie aus. */
|
||||||
export async function deliverItem(itemId: string): Promise<{ ok: boolean; message: string }> {
|
export async function deliverItem(itemId: string): Promise<{ ok: boolean; message: string }> {
|
||||||
const it = await one<any>('SELECT id, result_path, filename, folder_id, job_id FROM items WHERE id=$1', [itemId]);
|
const it = await one<any>('SELECT id, result_path, filename, folder_id, job_id, has_alpha FROM items WHERE id=$1', [itemId]);
|
||||||
if (!it?.result_path) return { ok: false, message: 'Kein Ergebnis vorhanden.' };
|
if (!it?.result_path) return { ok: false, message: 'Kein Ergebnis vorhanden.' };
|
||||||
const target = await targetFor(it);
|
const target = await targetFor(it);
|
||||||
if (!target) { await query(`UPDATE items SET delivery_status='failed' WHERE id=$1`, [itemId]); return { ok: false, message: 'Kein Auslieferungsziel konfiguriert.' }; }
|
if (!target) { await query(`UPDATE items SET delivery_status='failed' WHERE id=$1`, [itemId]); return { ok: false, message: 'Kein Auslieferungsziel konfiguriert.' }; }
|
||||||
@@ -56,13 +80,14 @@ export async function deliverItem(itemId: string): Promise<{ ok: boolean; messag
|
|||||||
await query(`UPDATE items SET delivery_status='pending' WHERE id=$1`, [itemId]);
|
await query(`UPDATE items SET delivery_status='pending' WHERE id=$1`, [itemId]);
|
||||||
try {
|
try {
|
||||||
const gallery = await galleryFor(it);
|
const gallery = await galleryFor(it);
|
||||||
const buf = await getObject(it.result_path);
|
const raw = await getObject(it.result_path);
|
||||||
await uploadBuffer(cfg, gallery, it.filename || `${it.id}.png`, buf);
|
const { buf, name } = await deliverableBuffer(raw, it.filename || `${it.id}.png`, !!it.has_alpha);
|
||||||
|
await uploadBuffer(cfg, gallery, name, buf);
|
||||||
// Optional: Metadaten als begleitende .md-Datei — pro Quelle steuerbar.
|
// Optional: Metadaten als begleitende .md-Datei — pro Quelle steuerbar.
|
||||||
if (await sidecarForKey(key)) {
|
if (await sidecarForKey(key)) {
|
||||||
try {
|
try {
|
||||||
const { buildMetadataMd, sidecarName } = await import('./metadata');
|
const { buildMetadataMd, sidecarName } = await import('./metadata');
|
||||||
await uploadBuffer(cfg, gallery, sidecarName(it.filename), Buffer.from(await buildMetadataMd(itemId), 'utf8'));
|
await uploadBuffer(cfg, gallery, sidecarName(name), Buffer.from(await buildMetadataMd(itemId), 'utf8'));
|
||||||
} catch (e) { console.error('[delivery] Metadaten-Beileger fehlgeschlagen', e); }
|
} catch (e) { console.error('[delivery] Metadaten-Beileger fehlgeschlagen', e); }
|
||||||
}
|
}
|
||||||
await query(`UPDATE items SET delivery_status='delivered', delivered_at=now() WHERE id=$1`, [itemId]);
|
await query(`UPDATE items SET delivery_status='delivered', delivered_at=now() WHERE id=$1`, [itemId]);
|
||||||
|
|||||||
+12
-2
@@ -93,11 +93,21 @@ export function slugify(s: string): string {
|
|||||||
|
|
||||||
const GENERIC = new Set(['', 'foto', 'photo', 'bild', 'image', 'img', 'telegram', 'unbenannt', 'klarbild', 'screenshot', 'pxl', 'whatsapp']);
|
const GENERIC = new Set(['', 'foto', 'photo', 'bild', 'image', 'img', 'telegram', 'unbenannt', 'klarbild', 'screenshot', 'pxl', 'whatsapp']);
|
||||||
|
|
||||||
|
/** Motiv aus einem beliebigen (evtl. schon verarbeiteten) Namen herausschälen:
|
||||||
|
* Datumspräfixe, UUIDs und lange Hex-Ketten entfernen, damit keine Doppelungen entstehen. */
|
||||||
|
function cleanMotif(name: string | null): string {
|
||||||
|
let s = (name || '').replace(/\.[^.]+$/, '');
|
||||||
|
s = s.replace(/\b\d{4}-\d{2}-\d{2}\b/g, ' '); // Datum
|
||||||
|
s = s.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ' '); // UUID
|
||||||
|
s = s.replace(/\b[0-9a-f]{16,}\b/gi, ' '); // lange Hex-Ketten
|
||||||
|
return slugify(s);
|
||||||
|
}
|
||||||
|
|
||||||
/** Logischer Dateiname: JJJJ-MM-TT_<motiv>_<format>.<ext>
|
/** Logischer Dateiname: JJJJ-MM-TT_<motiv>_<format>.<ext>
|
||||||
* z. B. 2026-07-23_lea_the-frame.png */
|
* z. B. 2026-07-23_lea_the-frame.png */
|
||||||
export function buildResultFilename(originalName: string | null, formatToken: string, ext = 'png'): string {
|
export function buildResultFilename(originalName: string | null, formatToken: string, ext = 'png'): string {
|
||||||
const raw = slugify((originalName || '').replace(/\.[^.]+$/, ''));
|
const raw = cleanMotif(originalName);
|
||||||
const motif = GENERIC.has(raw) ? 'klarbild' : raw;
|
const motif = (!raw || GENERIC.has(raw)) ? 'klarbild' : raw;
|
||||||
const date = new Date().toISOString().slice(0, 10);
|
const date = new Date().toISOString().slice(0, 10);
|
||||||
const fmt = slugify(formatToken) || 'original';
|
const fmt = slugify(formatToken) || 'original';
|
||||||
return `${date}_${motif}_${fmt}.${ext}`;
|
return `${date}_${motif}_${fmt}.${ext}`;
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import posixpath from 'node:path/posix';
|
import posixpath from 'node:path/posix';
|
||||||
import { loadConfig } from '../../../lib/picdrop';
|
import { loadConfig, cleanupTmp } from '../../../lib/picdrop';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
const json = (b: unknown, s = 200) =>
|
const json = (b: unknown, s = 200) =>
|
||||||
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
|
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
|
||||||
|
|
||||||
/** Diagnose: listet einen Picdrop-Ordner (Standard: Basisordner). Body { path? } */
|
/** Diagnose: listet einen Picdrop-Ordner (Standard: Basisordner). Body { path? }
|
||||||
|
* Aufräumen: Body { action:'cleanup_tmp', gallery } entfernt liegengebliebene .tmp-Dateien. */
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
const b = await request.json().catch(() => ({} as any));
|
const b = await request.json().catch(() => ({} as any));
|
||||||
const cfg = await loadConfig();
|
const cfg = await loadConfig();
|
||||||
if (!cfg) return json({ error: 'Picdrop nicht konfiguriert.' }, 400);
|
if (!cfg) return json({ error: 'Picdrop nicht konfiguriert.' }, 400);
|
||||||
|
if (b.action === 'cleanup_tmp') {
|
||||||
|
try {
|
||||||
|
const removed = await cleanupTmp(cfg, b.gallery || '');
|
||||||
|
return json({ cleaned: removed });
|
||||||
|
} catch (e: any) { return json({ error: e?.message || 'Aufräumen fehlgeschlagen.' }, 500); }
|
||||||
|
}
|
||||||
const path = b.path || cfg.basePath || '/';
|
const path = b.path || cfg.basePath || '/';
|
||||||
try {
|
try {
|
||||||
if (cfg.protocol === 'sftp') {
|
if (cfg.protocol === 'sftp') {
|
||||||
|
|||||||
Reference in New Issue
Block a user