From 6f6682457114c832a4a0f07b74e5b1980a4302b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kore=C5=84?= Date: Mon, 13 Jul 2026 22:41:24 +0200 Subject: [PATCH] vid support --- .gitignore | 4 ++ README.md | 15 ++++++ back/.gitignore | 2 + back/src/lib/crateClip.ts | 102 ++++++++++++++++++++++++++++++----- back/src/lib/nextcloud.ts | 57 ++++++++++++++++---- back/src/routes/protected.ts | 12 ++--- back/src/types/express.d.ts | 1 + bun.lock | 67 +++++++++++++++++++++++ front/Dockerfile | 35 ++++++++++-- package.json | 18 +++++++ tsconfig.json | 29 ++++++++++ 11 files changed, 307 insertions(+), 35 deletions(-) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bun.lock create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8cac331 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +pictures/ +out/ + +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..dae8eed --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# papryk-web + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.9. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/back/.gitignore b/back/.gitignore index 9664d7a..82e6eb2 100644 --- a/back/.gitignore +++ b/back/.gitignore @@ -1,6 +1,8 @@ # dependencies (bun install) node_modules downloads +downloads_vids +frames.txt *.mp3 *.mp4 diff --git a/back/src/lib/crateClip.ts b/back/src/lib/crateClip.ts index a8f400b..f7286fb 100644 --- a/back/src/lib/crateClip.ts +++ b/back/src/lib/crateClip.ts @@ -1,27 +1,78 @@ import { execa } from "execa"; import ffmpegPath from "ffmpeg-static"; +import { globSync, mkdirSync, readdirSync, rmSync, statSync, writeFile, writeFileSync } from "node:fs"; import path from "node:path"; +import { LOCAL_DIR, LOCAL_DIR_VID, LOCAL_DIR_VID_EXTRACTED } from "./nextcloud"; -export async function encodeFrames({ - inputPattern, - output, - fps = 30, -}: { - inputPattern: string; - output: string; - fps?: number; -}) { +export async function createVid( + fps: number, +) { + const entries = readdirSync(LOCAL_DIR_VID); + const videos = ( + await Promise.all( + entries.map(async (entry) => { + const fullPath = path.join(LOCAL_DIR_VID, entry); + const info = statSync(fullPath); + + if (!info.isFile()) { + return null; + } + + if (!entry.toLowerCase().endsWith(".mp4")) { + return null; + } + + return fullPath; + }) + ) + ).filter(Boolean) as string[]; + + for (const file of globSync(`${LOCAL_DIR_VID_EXTRACTED}/*`)) { + rmSync(file, { recursive: true, force: true }); + } + await Promise.all( + videos.map((video) => + extractFrames(video, LOCAL_DIR_VID_EXTRACTED, 1) + ) + ); + + + encodeFrames([ + `${LOCAL_DIR}/*.jpg`, + `${LOCAL_DIR}/*.png`, + `${LOCAL_DIR_VID_EXTRACTED}/*.jpg`, + ], path.resolve("./out.mp4"), fps) +} + +export async function encodeFrames( + inputPatterns: string[], + output: string, + fps: number = 30, +) { console.log("kleje filmik"); + const files = inputPatterns + .flatMap(pattern => globSync(pattern)) + .sort(); + + const concatFile = files + .map(file => `file '${file}'\nduration ${1/fps}`) + .join("\n"); + + + writeFileSync("./frames.txt", concatFile); + await execa(ffmpegPath!, [ "-y", - "-pattern_type", "glob", - "-framerate", fps.toString(), - "-i", inputPattern.toString(), + // "-pattern_type", "glob", + // "-framerate", fps.toString(), + "-f", "concat", + "-safe", "0", + "-i", "./frames.txt", "-i", path.resolve("./music.mp3"), "-vf", - "transpose=2,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2", + `transpose=2,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2`, // "-autorotate", "-c:v", "libx264", @@ -37,3 +88,28 @@ export async function encodeFrames({ ]); console.log("filmik sklejony"); } + +export async function extractFrames( + input: string, + outputDir: string, + targetFps: number, +): Promise { + if (!ffmpegPath) { + throw new Error("ffmpeg-static binary not found."); + } + + mkdirSync(outputDir, { recursive: true }); + + const fileName = path.basename(path.resolve(input)); + const outputPattern = path.join(outputDir, `${fileName}_frame_%06d.jpg`); + + await execa(ffmpegPath!, [ + "-y", "-i", + input, + "-vf", + `fps=${targetFps}`, + "-q:v", + "2", // JPEG quality (2 = very high) + outputPattern, + ]) +} diff --git a/back/src/lib/nextcloud.ts b/back/src/lib/nextcloud.ts index 298bd99..b3a198e 100644 --- a/back/src/lib/nextcloud.ts +++ b/back/src/lib/nextcloud.ts @@ -1,12 +1,19 @@ -import { createClient } from "webdav"; +import { createClient, type FileStat } from "webdav"; import fs from "fs/promises"; import path from "path"; export const REMOTE_DIR = "/"; export const LOCAL_DIR = path.resolve("./downloads"); +export const LOCAL_DIR_VID = path.resolve("./downloads_vids"); +export const LOCAL_DIR_VID_EXTRACTED = path.join(path.resolve("./downloads_vids"), "extracted"); export async function downloadMissing(remoteDir: string = REMOTE_DIR) { - await fs.mkdir(LOCAL_DIR, { recursive: true }) + await Promise.all([ + fs.mkdir(LOCAL_DIR, { recursive: true }), + fs.mkdir(LOCAL_DIR_VID, { recursive: true }), + fs.mkdir(LOCAL_DIR_VID_EXTRACTED, { recursive: true }), + ]); + const client = createClient( "https://nextcloud.papryk.com/public.php/dav/files/ckkL3Pg4X4Ye6pz", { @@ -14,16 +21,48 @@ export async function downloadMissing(remoteDir: string = REMOTE_DIR) { password: "", } ); + const contents = await client.getDirectoryContents(remoteDir); - const exisitngFiles = await fs.readdir(LOCAL_DIR); + const [ + existingImgFiles, + existingVidFiles, + ] = await Promise.all([ + fs.readdir(LOCAL_DIR), + fs.readdir(LOCAL_DIR_VID), + ]); - const missing = contents.filter(f => !exisitngFiles.includes(f.basename)); + const existingImgs = new Set(existingImgFiles); + const existingVids = new Set(existingVidFiles); - console.log(missing); + const missingImages = contents.filter( + ({ basename, mime }) => + mime?.startsWith("image/") && + !existingImgs.has(basename) + ); + const missingVideos = contents.filter( + ({ basename, mime }) => + mime?.startsWith("video/") && + !existingVids.has(basename) + ); + + await Promise.all([ + downloadToDir(missingImages, LOCAL_DIR), + downloadToDir(missingVideos, LOCAL_DIR_VID), + ]); +} + +async function downloadToDir(files: FileStat[], dir: string) { + const client = createClient( + "https://nextcloud.papryk.com/public.php/dav/files/ckkL3Pg4X4Ye6pz", + { + username: "ckkL3Pg4X4Ye6pz", + password: "", + } + ); let i = 0; - for (const item of missing) { + for (const item of files) { const result = await client.getFileContents(item.filename, { format: "binary", }); @@ -51,12 +90,10 @@ export async function downloadMissing(remoteDir: string = REMOTE_DIR) { buffer = Buffer.from(result as any); } - const localPath = path.resolve(path.join(LOCAL_DIR, item.filename)) + const localPath = path.resolve(path.join(dir, item.filename)) await fs.writeFile(localPath, buffer); i++; - console.log(`downloaded ${i}/${missing.length} ${item.basename}`) + console.log(`downloaded ${i}/${files.length} ${item.basename}`) } - - } diff --git a/back/src/routes/protected.ts b/back/src/routes/protected.ts index 3ed369b..b2af425 100644 --- a/back/src/routes/protected.ts +++ b/back/src/routes/protected.ts @@ -2,7 +2,7 @@ import { Router } from "express"; import { downloadMissing, LOCAL_DIR } from "../lib/nextcloud"; import path from "node:path"; -import { encodeFrames } from "../lib/crateClip"; +import { createVid, encodeFrames } from "../lib/crateClip"; import fs from "node:fs" const router = Router(); @@ -25,13 +25,11 @@ router.get("/video", async (req, res) => { router.post("/createclip", async (req, res) => { try { - await encodeFrames({ - inputPattern: `${LOCAL_DIR}/*.jpg`, - output: path.resolve("./out.mp4"), - fps: 8, - }); + console.log("create clip request") + await createVid(8) res.status(200).end(); - } catch { + } catch (e) { + console.log(e) res.status(501).end(); } }) diff --git a/back/src/types/express.d.ts b/back/src/types/express.d.ts index 9b23dc4..a3e2769 100644 --- a/back/src/types/express.d.ts +++ b/back/src/types/express.d.ts @@ -10,3 +10,4 @@ declare module "express-serve-static-core" { } | string | jwt.JwtPayload; } } + diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..2c8bed1 --- /dev/null +++ b/bun.lock @@ -0,0 +1,67 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "papryk-web", + "devDependencies": { + "@types/bun": "latest", + "concurrently": "^10.0.3", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "concurrently": ["concurrently@10.0.3", "", { "dependencies": { "chalk": "5.6.2", "rxjs": "7.8.2", "shell-quote": "1.8.4", "supports-color": "10.2.2", "tree-kill": "1.2.2", "yargs": "18.0.0" }, "bin": { "conc": "dist/bin/index.js", "concurrently": "dist/bin/index.js" } }, "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], + + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + } +} diff --git a/front/Dockerfile b/front/Dockerfile index 25fa59e..4736ab0 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,9 +1,29 @@ -FROM oven/bun:latest +# FROM oven/bun:latest +# +# WORKDIR /app +# +# COPY package.json bun.lockb* ./ +# RUN bun install +# +# COPY . . +# +# ARG VITE_API_URL +# ENV VITE_API_URL=$VITE_API_URL +# +# RUN rm -f .env .env.* +# +# RUN bun run build --mode production +# +# EXPOSE 5173 +# +# CMD ["bun", "run", "preview", "--", "--host", "--port", "5173"] +# Build stage +FROM oven/bun:latest AS builder WORKDIR /app COPY package.json bun.lockb* ./ -RUN bun install +RUN bun install --frozen-lockfile COPY . . @@ -11,9 +31,14 @@ ARG VITE_API_URL ENV VITE_API_URL=$VITE_API_URL RUN rm -f .env .env.* - RUN bun run build --mode production -EXPOSE 5173 -CMD ["bun", "run", "preview", "--", "--host", "--port", "5173"] +# Production stage +FROM nginx:alpine + +COPY --from=builder /app/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/package.json b/package.json new file mode 100644 index 0000000..df20c40 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "papryk-web", + "module": "index.ts", + "type": "module", + "private": true, + "scripts": { + "deploy:web": "cd front && bun run dev", + "deploy:api": "cd back && bun run dev", + "dev": "concurrently \"bun run deploy:web\" \"bun run deploy:api\"" + }, + "devDependencies": { + "@types/bun": "latest", + "concurrently": "^10.0.3" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}