vid support
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
pictures/
|
||||
out/
|
||||
|
||||
node_modules
|
||||
15
README.md
Normal file
15
README.md
Normal file
@@ -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.
|
||||
2
back/.gitignore
vendored
2
back/.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
downloads
|
||||
downloads_vids
|
||||
frames.txt
|
||||
*.mp3
|
||||
*.mp4
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
|
||||
1
back/src/types/express.d.ts
vendored
1
back/src/types/express.d.ts
vendored
@@ -10,3 +10,4 @@ declare module "express-serve-static-core" {
|
||||
} | string | jwt.JwtPayload;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
67
bun.lock
Normal file
67
bun.lock
Normal file
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -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;"]
|
||||
|
||||
18
package.json
Normal file
18
package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user