fix(backend): enhance sequence discovery from sister website directories and sync in post-receive
This commit is contained in:
1 parent
0bf544720b
commit
ca79692b3e
3 files changed
+106
-45
No files matched your search
@@ -35,7 +35,7 @@ import { crmRoute } from "./routes/crm";
|
||||
import { brandKnowledgeRoute } from "./routes/brandKnowledge";
|
||||
import { categorySectionsRoute } from "./routes/categorySections";
|
||||
import { llmPromptsRoute } from "./routes/llmPrompts";
|
||||
import { frameSequencesRoute } from "./routes/frame-sequences";
|
||||
import { frameSequencesRoute, getCandidateSequenceDirs } from "./routes/frame-sequences";
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3051;
|
||||
const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173";
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_brand";
|
||||
@@ -142,9 +142,16 @@ const app = new Elysia()
|
||||
}
|
||||
return file;
|
||||
})
|
||||
.get("/frame-sequences/:sequence/:filename", ({ params: { sequence, filename }, set }) => {
|
||||
const file = Bun.file(`public/frame-sequences/${sequence}/${filename}`);
|
||||
.get("/frame-sequences/:sequence/:filename", async ({ params: { sequence, filename }, set }) => {
|
||||
let file = Bun.file(`public/frame-sequences/${sequence}/${filename}`);
|
||||
if (file.size === 0) {
|
||||
const candidateDirs = await getCandidateSequenceDirs();
|
||||
for (const d of candidateDirs) {
|
||||
const altFile = Bun.file(`${d}/${sequence}/${filename}`);
|
||||
if (altFile.size > 0) {
|
||||
return altFile;
|
||||
}
|
||||
}
|
||||
set.status = 404;
|
||||
return "File not found";
|
||||
}
|
||||
|
||||
@@ -19,61 +19,111 @@ export interface FrameSequenceManifest {
|
||||
|
||||
const getBaseDir = () => join(process.cwd(), "public", "frame-sequences");
|
||||
|
||||
export const getCandidateSequenceDirs = async (): Promise<string[]> => {
|
||||
const candidates: string[] = [
|
||||
getBaseDir(),
|
||||
join(process.cwd(), "..", "website", "public", "frame-sequences"),
|
||||
join(process.cwd(), "..", "..", "website", "public", "frame-sequences"),
|
||||
];
|
||||
|
||||
try {
|
||||
// Check sister projects in deployment layout (e.g. /home/ti/projects/ddc-website/apps/website/public/frame-sequences)
|
||||
const possibleProjectRoots = [
|
||||
join(process.cwd(), "..", ".."),
|
||||
join(process.cwd(), "..", "..", ".."),
|
||||
];
|
||||
|
||||
for (const root of possibleProjectRoots) {
|
||||
if (existsSync(root)) {
|
||||
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
||||
for (const e of entries) {
|
||||
if (e.isDirectory() && (e.name.includes("website") || e.name.includes("shop"))) {
|
||||
candidates.push(join(root, e.name, "apps", "website", "public", "frame-sequences"));
|
||||
candidates.push(join(root, e.name, "public", "frame-sequences"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// non-fatal
|
||||
}
|
||||
|
||||
return Array.from(new Set(candidates.filter((d) => existsSync(d))));
|
||||
};
|
||||
|
||||
export const frameSequencesRoute = new Elysia({ prefix: "/frame-sequences" })
|
||||
// 1. List all Frame Sequences (Reads manifests from public/frame-sequences/*)
|
||||
// 1. List all Frame Sequences (Reads manifests from public/frame-sequences/* and candidate directories)
|
||||
.get("/", async () => {
|
||||
const baseDir = getBaseDir();
|
||||
if (!existsSync(baseDir)) {
|
||||
await mkdir(baseDir, { recursive: true });
|
||||
return { sequences: [] };
|
||||
await mkdir(baseDir, { recursive: true }).catch(() => {});
|
||||
}
|
||||
|
||||
const entries = await readdir(baseDir, { withFileTypes: true });
|
||||
const searchDirs = await getCandidateSequenceDirs();
|
||||
const seenSlugs = new Set<string>();
|
||||
const sequences: FrameSequenceManifest[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const folderPath = join(baseDir, entry.name);
|
||||
const manifestPath = join(folderPath, "manifest.json");
|
||||
|
||||
if (existsSync(manifestPath)) {
|
||||
try {
|
||||
const raw = await readFile(manifestPath, "utf8");
|
||||
const manifest: FrameSequenceManifest = JSON.parse(raw);
|
||||
sequences.push(manifest);
|
||||
continue;
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse manifest in ${entry.name}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detect frames if no manifest exists
|
||||
for (const dir of searchDirs) {
|
||||
try {
|
||||
const files = await readdir(folderPath);
|
||||
const imageFiles = files
|
||||
.filter((f) => /\.(webp|png|jpe?g)$/i.test(f) && !f.startsWith("."))
|
||||
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (seenSlugs.has(entry.name)) continue;
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
const autoManifest: FrameSequenceManifest = {
|
||||
id: `seq_${entry.name}`,
|
||||
name: entry.name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
||||
slug: entry.name,
|
||||
folderPath: `/frame-sequences/${entry.name}`,
|
||||
pattern: "frame-{index}.webp",
|
||||
padding: 3,
|
||||
totalFrames: imageFiles.length,
|
||||
fps: 24,
|
||||
durationSeconds: parseFloat((imageFiles.length / 24).toFixed(1)),
|
||||
posterUrl: `/frame-sequences/${entry.name}/${imageFiles[0]}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const folderPath = join(dir, entry.name);
|
||||
const manifestPath = join(folderPath, "manifest.json");
|
||||
|
||||
await writeFile(manifestPath, JSON.stringify(autoManifest, null, 2), "utf8");
|
||||
sequences.push(autoManifest);
|
||||
if (existsSync(manifestPath)) {
|
||||
try {
|
||||
const raw = await readFile(manifestPath, "utf8");
|
||||
const manifest: FrameSequenceManifest = JSON.parse(raw);
|
||||
seenSlugs.add(entry.name);
|
||||
sequences.push(manifest);
|
||||
|
||||
// Auto-sync manifest to local backend baseDir if not present
|
||||
const localSeqDir = join(baseDir, entry.name);
|
||||
if (!existsSync(localSeqDir)) {
|
||||
await mkdir(localSeqDir, { recursive: true }).catch(() => {});
|
||||
await writeFile(join(localSeqDir, "manifest.json"), raw, "utf8").catch(() => {});
|
||||
}
|
||||
continue;
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse manifest in ${entry.name}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detect frames if no manifest exists
|
||||
try {
|
||||
const files = await readdir(folderPath);
|
||||
const imageFiles = files
|
||||
.filter((f) => /\.(webp|png|jpe?g)$/i.test(f) && !f.startsWith("."))
|
||||
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
const autoManifest: FrameSequenceManifest = {
|
||||
id: `seq_${entry.name}`,
|
||||
name: entry.name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
||||
slug: entry.name,
|
||||
folderPath: `/frame-sequences/${entry.name}`,
|
||||
pattern: "frame-{index}.webp",
|
||||
padding: 3,
|
||||
totalFrames: imageFiles.length,
|
||||
fps: 24,
|
||||
durationSeconds: parseFloat((imageFiles.length / 24).toFixed(1)),
|
||||
posterUrl: `/frame-sequences/${entry.name}/${imageFiles[0]}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await writeFile(manifestPath, JSON.stringify(autoManifest, null, 2), "utf8").catch(() => {});
|
||||
seenSlugs.add(entry.name);
|
||||
sequences.push(autoManifest);
|
||||
}
|
||||
} catch (scanErr) {
|
||||
console.error(`Error scanning sequence folder ${entry.name}`, scanErr);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error scanning sequence folder ${entry.name}`, err);
|
||||
// Continue to next candidate dir
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user