Compare commits

...

3 Commits

Author SHA1 Message Date
Patryk Koreń
af5112b95c fix 2026-07-13 22:50:17 +02:00
Patryk Koreń
4988491ec2 vid support 2026-07-13 22:43:05 +02:00
Patryk Koreń
6f66824571 vid support 2026-07-13 22:41:24 +02:00
10 changed files with 275 additions and 31 deletions

4
.gitignore vendored
View File

@@ -1,2 +1,4 @@
out/
pictures/
out/
node_modules

15
README.md Normal file
View 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
View File

@@ -1,6 +1,8 @@
# dependencies (bun install)
node_modules
downloads
downloads_vids
frames.txt
*.mp3
*.mp4

View File

@@ -1,27 +1,79 @@
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";
import { out_vid_path } from "../routes/protected";
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`,
], out_vid_path, 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 +89,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,
])
}

View File

@@ -1,13 +1,20 @@
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 const OUT_DIR = path.resolve("./out");
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",
{
@@ -15,16 +22,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",
});
@@ -52,12 +91,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}`)
}
}

View File

@@ -2,7 +2,7 @@
import { Router } from "express";
import { downloadMissing, LOCAL_DIR, OUT_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();
@@ -27,12 +27,8 @@ router.get("/video", async (req, res) => {
router.post("/createclip", async (req, res) => {
try {
await fs.mkdirSync(OUT_DIR, { recursive: true })
await encodeFrames({
inputPattern: `${LOCAL_DIR}/*.jpg`,
output: out_vid_path,
fps: 8,
});
console.log("create clip request")
await createVid(8)
res.status(200).end();
} catch (e) {
console.log(e)

View File

@@ -10,3 +10,4 @@ declare module "express-serve-static-core" {
} | string | jwt.JwtPayload;
}
}

67
bun.lock Normal file
View 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=="],
}
}

18
package.json Normal file
View 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
View 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
}
}