diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index d562658..a5df628 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -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"; } diff --git a/apps/backend/src/routes/frame-sequences.ts b/apps/backend/src/routes/frame-sequences.ts index 63bf2ae..8dd56a0 100644 --- a/apps/backend/src/routes/frame-sequences.ts +++ b/apps/backend/src/routes/frame-sequences.ts @@ -19,61 +19,111 @@ export interface FrameSequenceManifest { const getBaseDir = () => join(process.cwd(), "public", "frame-sequences"); +export const getCandidateSequenceDirs = async (): Promise => { + 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(); 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 } } diff --git a/serverinfo/post-receive b/serverinfo/post-receive index 851765e..545c830 100644 --- a/serverinfo/post-receive +++ b/serverinfo/post-receive @@ -90,6 +90,10 @@ $BUN install # สร้าง public/ folder สำหรับ static files (upload images) mkdir -p $TARGET_BACKEND/apps/backend/public +if [ -d "$TEMP_DIR/apps/backend/public/frame-sequences" ]; then + mkdir -p $TARGET_BACKEND/apps/backend/public/frame-sequences + rsync -av "$TEMP_DIR/apps/backend/public/frame-sequences/" "$TARGET_BACKEND/apps/backend/public/frame-sequences/" +fi cd $TARGET_BACKEND/apps/backend