added backend to scan for and serve files
This commit is contained in:
Generated
+1184
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start:static": "ts-node src/index",
|
||||
"start": "nodemon --watch 'src/**/*.ts' --ext ts --exec npm run start:static",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fastify": "^4.26.1",
|
||||
"ffprobe": "^1.1.2",
|
||||
"mathjs": "^12.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ffprobe": "^1.1.8",
|
||||
"@types/mathjs": "^9.4.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const config = {
|
||||
directories: {
|
||||
video: '/home/anton/Documents/TG/Панды/unsorted input',
|
||||
audio: '/home/anton/Documents/TG/Панды/music',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import Fastify, { FastifyRequest } from 'fastify';
|
||||
import { Video } from './model/video';
|
||||
|
||||
(async () => {
|
||||
|
||||
const fastify = Fastify({
|
||||
logger: true
|
||||
});
|
||||
|
||||
fastify.get('/video/all', async function handler(request, reply) {
|
||||
return Video.list();
|
||||
});
|
||||
|
||||
fastify.get('/video/:hash([0-9a-fA-F]+)', async function handler(request: FastifyRequest<{ Params: { hash: string; }; }>, reply) {
|
||||
const hash = request.params.hash;
|
||||
const file = await Video.getByHash(hash);
|
||||
if (!file)
|
||||
reply.callNotFound();
|
||||
else
|
||||
return file;
|
||||
});
|
||||
|
||||
fastify.get('/video/:hash([0-9a-fA-F]+)/content', async function handler(request: FastifyRequest<{ Params: { hash: string; }; }>, reply) {
|
||||
const hash = request.params.hash;
|
||||
const file = await Video.getByHash(hash);
|
||||
if (!file)
|
||||
reply.callNotFound();
|
||||
else {
|
||||
reply.header('Content-Disposition', 'inline; filename=' + file.name);
|
||||
reply.header('Content-Length', file.size);
|
||||
reply.type(file.mimeType);
|
||||
reply.send(await Video.getContentsByPath(file.path));
|
||||
return reply;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await fastify.listen({ port: 5174 });
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
})()
|
||||
.catch((err) => console.error);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import { readFileSync, createReadStream } from 'fs';
|
||||
import { Readable } from 'stream';
|
||||
import * as math from 'mathjs';
|
||||
import { basename, join } from 'path';
|
||||
import ffprobe, { FFProbeResult } from 'ffprobe';
|
||||
|
||||
import { config } from '../config';
|
||||
import { hashString } from '../utils';
|
||||
|
||||
export type VideoFileMeta = {
|
||||
codec: string;
|
||||
frameRate: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type VideoFile = {
|
||||
hash: string;
|
||||
path: string;
|
||||
name: string;
|
||||
size: number;
|
||||
mimeType: string;
|
||||
meta: VideoFileMeta;
|
||||
rawMeta?: FFProbeResult;
|
||||
};
|
||||
|
||||
export class Video {
|
||||
public static async list(): Promise<VideoFile[]> {
|
||||
const dir = config.directories.video;
|
||||
|
||||
const files = await fs.readdir(dir, { recursive: true, withFileTypes: true });
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
files
|
||||
.filter((f) => f.isFile())
|
||||
.map((f) => Video.getFileDescription(join(dir, f.name)))
|
||||
)
|
||||
)
|
||||
.filter((vf) => !!vf) as VideoFile[];
|
||||
}
|
||||
|
||||
public static async getFileDescription(path: string): Promise<VideoFile | undefined> {
|
||||
try {
|
||||
const meta = await ffprobe(path, { path: '/usr/bin/ffprobe' });
|
||||
|
||||
const videoStreams = meta.streams.filter((st) => st.codec_type === 'video');
|
||||
if (videoStreams.length > 1)
|
||||
throw new Error('Files with multiple video streams are not supported');
|
||||
|
||||
const videoStream = videoStreams[0];
|
||||
|
||||
const name = basename(path, '');
|
||||
|
||||
return {
|
||||
hash: hashString(name),
|
||||
path,
|
||||
name,
|
||||
size: (await fs.stat(path)).size,
|
||||
mimeType: 'video/mp4', // TODO
|
||||
// rawMeta: meta,
|
||||
meta: {
|
||||
codec: videoStream.codec_name ?? '',
|
||||
frameRate: math.evaluate(videoStream.r_frame_rate),
|
||||
width: videoStream.width ?? 0,
|
||||
height: videoStream.height ?? 0,
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`Error reading video file "${path}" meta: ${String(err)}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getByHash(hash: string): Promise<VideoFile | undefined> {
|
||||
const files = await Video.list();
|
||||
const file = files.find((f) => f.hash == hash);
|
||||
return file;
|
||||
}
|
||||
|
||||
public static async getContentsByPath(path: string): Promise<Readable> {
|
||||
const stream = createReadStream(path);
|
||||
|
||||
// const buffer = readFileSync(path);
|
||||
// const stream = new Readable({
|
||||
// read() {
|
||||
// this.push(buffer);
|
||||
// this.push(null);
|
||||
// }
|
||||
// });
|
||||
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
export function hashString(str: string): string {
|
||||
return crypto.createHash('md5').update(str).digest('hex');
|
||||
}
|
||||
Reference in New Issue
Block a user