Barely working

This commit is contained in:
2021-12-30 01:31:10 +03:00
commit 9c823eb3a4
11 changed files with 11143 additions and 0 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
PORT=3000
KEY=w2geth

13
.eslintrc.js Normal file
View File

@@ -0,0 +1,13 @@
module.exports = {
extends: [
'airbnb-typescript/base',
'plugin:import/recommended'
],
parserOptions: {
project: './tsconfig.json',
},
rules: {
'import/prefer-default-export': 'off',
},
ignorePatterns: ['.eslintrc.js']
};

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
.vscode/
files/**
temp/
.env

10844
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "watch2gether-core",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "npm run dev-core & npm run dev-stream",
"dev-core": "nodemon ./src/index.ts",
"dev-stream": "nodemon ./src/streamMovies.ts"
},
"author": "Anatoly Kopyl",
"license": "ISC",
"dependencies": {
"@koa/cors": "^3.1.0",
"@types/mongoose": "^5.11.97",
"dotenv": "^10.0.0",
"koa": "^2.13.4",
"koa-bodyparser": "^4.3.0",
"koa-json": "^2.0.2",
"koa-router": "^10.1.1",
"mongoose": "^6.1.3",
"mv": "^2.1.1",
"short-uuid": "^4.2.0",
"webtorrent": "^1.5.8"
},
"devDependencies": {
"@types/koa": "^2.13.4",
"@types/koa__cors": "^3.1.1",
"@types/koa-bodyparser": "^4.3.5",
"@types/koa-json": "^2.0.20",
"@types/koa-router": "^7.4.4",
"@types/mv": "^2.1.2",
"@types/node": "^17.0.4",
"@types/webtorrent": "^0.109.2",
"@typescript-eslint/eslint-plugin": "^5.8.0",
"eslint": "^8.5.0",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-plugin-import": "^2.25.3",
"nodemon": "^2.0.15",
"ts-node": "^10.4.0",
"typescript": "^4.5.4"
}
}

0
src/clearRooms.ts Normal file
View File

110
src/index.ts Normal file
View File

@@ -0,0 +1,110 @@
import Koa from 'koa';
import Router from 'koa-router';
require('dotenv').config();
import json from 'koa-json';
import bodyParser from 'koa-bodyparser';
import cors from '@koa/cors';
import fs from 'fs';
import mv from 'mv';
import { model, connect } from 'mongoose';
import short from 'short-uuid';
import WebTorrent from 'webtorrent';
import { errorResponse, findInDir } from './utils';
import { Room, roomSchema } from './interfaces';
const RoomModel = model<Room>('Room', roomSchema);
const tCli = new WebTorrent();
const app = new Koa();
const router = new Router({ prefix: '/api' });
router.post('/room', async (ctx) => {
return new Promise(function (resolve) {
tCli.add(ctx.request.body.magnet, { path: process.env.TEMP_FILES }, async function (torrent) {
const room = {
id: (short()).new(),
magnet: ctx.request.body.magnet,
createdAt: new Date(),
movie: torrent.name,
position: 0,
};
const doc = new RoomModel(room);
torrent.on('done', function () {
findInDir(torrent.path, /\.mp4$/, (filename: string) => {
mv(`./${filename}`, `${process.env.FILES}/${room.id}.mp4`, () => {
doc.downloaded = true;
doc.downloadedAt = new Date();
doc.save();
fs.rmSync(__dirname + '/../' + torrent.path + '/' + torrent.name, { recursive: true });
});
});
});
await doc.save();
ctx.body = room;
resolve(null);
});
});
});
router.get('/room', async (ctx) => {
const room = await RoomModel.findOne({ id: ctx.request.query.id }).exec();
if (room) {
ctx.body = room;
} else {
({ status: ctx.status, body: ctx.body } = errorResponse('room-00', 'Room not found'));
}
});
router.post('/position', async (ctx) => {
await RoomModel.updateOne({ id: ctx.request.body.id }, { position: Number(ctx.request.body.position) });
ctx.body = 'success';
});
router.get('/position', async (ctx) => {
const room = await RoomModel.findOne({ id: ctx.request.query.id }).exec();
if (room) {
ctx.body = {
position: room.position,
};
} else {
({ status: ctx.status, body: ctx.body } = errorResponse('room-00', 'Room not found'));
}
});
// Лучше спрятать в вебсокет?
router.get('/status', async (ctx) => {
const room = await RoomModel.findOne({ id: ctx.request.query.id }).exec();
if (room) {
const torrent = tCli.get(room.magnet);
if (torrent) {
ctx.body = {
progress: (torrent as WebTorrent.Torrent).progress,
downloaded: room.downloaded,
};
} else {
({ status: ctx.status, body: ctx.body } = errorResponse('status-00', 'No torrent found'));
}
} else {
({ status: ctx.status, body: ctx.body } = errorResponse('status-01', 'No room found'));
}
});
app.use(json());
app.use(bodyParser());
app.use(cors({
credentials: true,
}));
app.use(router.routes());
app.use(router.allowedMethods());
connect(process.env.DB).then(() => {
app.listen(process.env.PORT);
console.log(`💡 Core api live on port ${process.env.PORT}`);
});

21
src/interfaces.ts Normal file
View File

@@ -0,0 +1,21 @@
import { Schema } from 'mongoose';
export interface Room {
id: string;
magnet: string;
createdAt: Date;
movie?: string;
downloaded?: boolean;
downloadedAt?: Date;
position: number;
}
export const roomSchema = new Schema<Room>({
id: { type: String, required: true },
magnet: { type: String, required: true },
createdAt: { type: Date, required: true },
movie: { type: String, required: false },
downloaded: { type: Boolean, required: false },
downloadedAt: { type: Date, required: false },
position: { type: Number, required: true },
});

59
src/streamMovies.ts Normal file
View File

@@ -0,0 +1,59 @@
import Koa from 'koa';
import Router from 'koa-router';
require('dotenv').config();
import json from 'koa-json';
import bodyParser from 'koa-bodyparser';
import cors from '@koa/cors';
import fs from 'fs';
import { errorResponse } from './utils';
const app = new Koa();
const router = new Router({ prefix: '/movie' });
router.get('/', async (ctx) => {
const range = ctx.req.headers.range;
if (!range) {
({ status: ctx.status, body: ctx.body } = errorResponse('stream-01', 'Requires range header'));
}
const videoPath = './files/' + ctx.request.query.id + '.mp4';
const videoSize = fs.statSync(videoPath).size;
// Parse Range
// Example: "bytes=32324-"
const CHUNK_SIZE = 10 ** 6; // 1MB
const start = Number(range.replace(/\D/g, ''));
const end = Math.min(start + CHUNK_SIZE, videoSize - 1);
// Create headers
const contentLength = end - start + 1;
const headers = {
'Content-Range': `bytes ${start}-${end}/${videoSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': contentLength,
'Content-Type': 'video/mp4',
};
// HTTP Status 206 for Partial Content
ctx.res.writeHead(206, headers);
// create video read stream for this particular chunk
const videoStream = fs.createReadStream(videoPath, { start, end });
// Stream the video chunk to the client
ctx.body = videoStream;
});
app.use(json());
app.use(bodyParser());
app.use(cors({
credentials: true,
}));
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(process.env.MOVIES_PORT);
console.log(`📼 Streaming server live on port ${process.env.MOVIES_PORT}`);

29
src/utils.ts Normal file
View File

@@ -0,0 +1,29 @@
import fs from 'fs';
import path from 'path';
export function errorResponse(code: String, message: String, status?: number) {
if (!status) {
status = 400;
}
return {
status,
body: { code, message },
};
}
export function findInDir(startPath: string, filter: RegExp, callback: Function){
if (!fs.existsSync(startPath)){
console.log('No such directory ', startPath);
return;
}
const files = fs.readdirSync(startPath);
for (let i = 0; i < files.length; i++){
const filename = path.join(startPath, files[i]);
const stat = fs.lstatSync(filename);
if (stat.isDirectory()){
findInDir(filename, filter, callback);
} else if (filter.test(filename)) callback(filename);
}
}

17
tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"noImplicitAny": true,
"outDir": "./dist",
"sourceMap": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
},
"include": [
"./src/**/*"
],
"exclude": [
"node_modules"
]
}