mirror of
https://github.com/anatolykopyl/movieroom-core.git
synced 2026-03-26 12:54:51 +00:00
Barely working
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
PORT=3000
|
||||
KEY=w2geth
|
||||
13
.eslintrc.js
Normal file
13
.eslintrc.js
Normal 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
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
.vscode/
|
||||
files/**
|
||||
temp/
|
||||
.env
|
||||
10844
package-lock.json
generated
Normal file
10844
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
package.json
Normal file
43
package.json
Normal 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
0
src/clearRooms.ts
Normal file
110
src/index.ts
Normal file
110
src/index.ts
Normal 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
21
src/interfaces.ts
Normal 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
59
src/streamMovies.ts
Normal 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
29
src/utils.ts
Normal 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
17
tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user