127.0.0.1:8000 watch-together / master server / routes / api / video.js
master

Tree @master (Download .tar.gz)

video.js @masterraw · history · blame

const utils = require('../../utils');
const time = require('../../utils/time');
const clamscan = require('../../utils/clamscan');
const ffmpeg = require('../../utils/ffmpeg');
const ws_utils = require('../../websocket/utils');
const broadcast = require('../../websocket/broadcast');
const upload_session = require('../../utils/upload_session');
const fs = require('fs');
const path = require('path');
const subtitle = require('subtitle');
const uploader = require('huge-uploader-nodejs');
const pretty_bytes = require('pretty-bytes');

module.exports = {
    post: {
        "subtitle": function(req, res) {
            let file_path;

            utils.validate.keys(req.body, [
                ['video_id', utils.validate.uuid]
            ]).then(function() {
                file_path = path.resolve(VIDEO_ROOT, req.body.video_id, "subtitle.json");
                return utils.fs.stat(file_path).catch(function() {
                    // Errors here just mean that the subtitle file does not exist
                    return utils.reject([utils.status.ok, {}]);
                });
            }).then(function() {
                return utils.fs.readFile(file_path);
            }).then(function(subtitle) {
                res.send(utils.ok({
                    subtitles: JSON.parse(subtitle.toString())
                }));
            }).catch(utils.handle_err.res(res, "Error getting subtitles"));
        },
        "upload": {
            "subtitle": function(req, res) {
                let video_dir;
                let subtitle_path;

                utils.validate.keys(req.body, [
                    ['video_id', utils.validate.uuid], 'subtitle_string'
                ]).then(function() {
                    video_dir = path.resolve(VIDEO_ROOT, req.body.video_id);
                    // Make sure the video we are uploading subtitles for actually exists
                    return utils.fs.stat(video_dir);
                }).then(function() {
                    subtitle_path = path.resolve(video_dir, "subtitle.json");
                    return utils.fs.writeFile(
                        subtitle_path,
                        JSON.stringify(subtitle.parse(req.body.subtitle_string))
                    );
                }).then(function() {
                    return clamscan.assert_safe(subtitle_path, {
                        ip: res.locals.ip,
                        user_id: req.user.user_id
                    });
                }).then(function() {
                    res.send(utils.ok());
                }).catch(utils.handle_err.res(res, "Error uploading subtitles"));
            },
            "request": function(req, res) {
                const TEMP_UPLOAD_DIR_USER = path.join(TEMP_UPLOAD_DIR, req.user.user_id);

                utils.validate.keys(req.body, [
                    ['bytes', bytes => typeof bytes === "number"], 'name'
                ]).then(function() {
                    if (upload_session.get(req.user)) {
                        return utils.reject("You already have an upload session elsewhere");
                    }
                    return utils.fs.mkdir_if_not_exists(TEMP_UPLOAD_DIR_USER);
                }).then(function() {
                    return utils.get_storage_remaining();
                }).then(function(bytes) {
                    let reserved_bytes = Object.values(upload_session.get()).reduce(function(reserved_bytes, upload) {
                        return reserved_bytes + upload.size;
                    }, 0);

                    // Multiply by 2 because there must be
                    // room for the converted file as well
                    if (req.body.bytes * 2 + reserved_bytes > bytes) {
                        return utils.reject(
                            `Not enough storage available on server. Max upload size is ${pretty_bytes((bytes - reserved_bytes) / 2)}`
                        );
                    }

                    upload_session.create(
                        req.user, req.body.name, req.body.bytes,
                        TEMP_UPLOAD_DIR_USER
                    );

                    res.send(utils.ok());
                }).catch(utils.handle_err.res(res, "Error requesting upload"));
            },
            "": [{
                rate_limit: false
            }, function(req, res) {
                let ongoing_upload = upload_session.get(req.user);

                if (!ongoing_upload) {
                    return utils.handle_err.res(res)("Error uploading file, no upload session");
                }

                uploader(
                    req,
                    ongoing_upload.temp_dir,
                    // bytes / 1000000
                    ongoing_upload.size, // Max total file size in MB
                    10 // Chunk size in MB
                ).then(function(assemble_chunks) {
                    // chunk written to disk
                    res.writeHead(204, 'No Content');
                    res.end();

                    if (!assemble_chunks) {
                        return utils.resolve();
                    }

                    // on last chunk, assembleChunks function is returned
                    // the response is already sent to the browser because it can take some time if the file is huge
                    res.locals.log.v("Final Chunk Uploaded");
                    let data = Object.assign({
                        user_id: req.user.user_id
                    }, ongoing_upload.video);

                    upload_session.delete(req.user);

                    const video_create_data = {
                        video_id: data.video_id,
                        name: data.name,
                        status: utils.status.video.reassembling,
                        expires: Date.now() + (time.one_day * 7),
                        created_by: req.user.user_id
                    };

                    return utils.query(
                        "INSERT INTO videos SET ?",
                        video_create_data
                    ).catch(utils.handle_err.sql(res)).then(function() {
                        broadcast({
                            command: "new-video",
                            data: video_create_data
                        }, {
                            [req.user.user_id]: true
                        });

                        return assemble_chunks();
                    }).then(function(upload_data) {
                        Object.assign(data, {
                            temp_file_path: upload_data.filePath
                        });

                        return clamscan.assert_safe(data.temp_file_path, {
                            ip: res.locals.ip,
                            user_id: req.user.user_id
                        });
                    }).then(function() {
                        return utils.query("UPDATE videos" + utils.set_where({
                            name: data.name,
                            status: utils.status.video.converting
                        }, {
                            video_id: data.video_id
                        })).catch(utils.handle_err.sql(res));
                    }).then(function() {
                        // Big Boi
                        return ffmpeg.convert(data, {
                            delete_source: true
                        });
                    }).then(function() {
                        res.locals.log.v(data.name, "converted!");
                        broadcast({
                            command: "video-status",
                            data: {
                                video_id: data.video_id,
                                status: utils.status.video.converting,
                                progress: 100
                            }
                        }, {
                            [req.user.user_id]: true
                        });
                    }).then(function() {
                        return utils.query("UPDATE videos" + utils.set_where({
                            status: utils.status.video.ready
                        }, {
                            video_id: data.video_id
                        })).catch(utils.handle_err.sql(res));
                    }).then(function() {
                        broadcast({
                            command: "video-status",
                            data: {
                                video_id: data.video_id,
                                status: utils.status.video.ready
                            }
                        }, {
                            [req.user.user_id]: true
                        });
                    }).catch(function(error) {
                        res.locals.log.general.error(error);

                        upload_session.delete(req.user);
                        return utils.rimraf(ongoing_upload.temp_dir).then(function() {
                            return utils.query(
                                "DELETE FROM videos WHERE video_id=?",
                                [data.video_id]
                            ).catch(utils.handle_err.sql(res, true));
                        }).then(function() {
                            broadcast({
                                command: "delete-video-response",
                                data: utils.ok({
                                    video_id: data.video_id,
                                    name: data.name,
                                    reason: "upload failed"
                                })
                            }, {
                                [req.user.user_id]: true
                            });
                        });
                    });
                }).catch(utils.handle_err.status_res(res)).finally(ws_utils.broadcast_storage);
            }]
        }
    },
    get: {
        ":video_id": {
            "": [{
                rate_limit: false
            }, function(req, res) {
                let file_path;
                utils.validate.keys(req.params, [
                    ['video_id', utils.validate.uuid]
                ]).then(function() {
                    file_path = path.resolve(VIDEO_ROOT, req.params.video_id, "video");
                    return utils.fs.stat(file_path);
                }).then(function(stat) {
                    const range = req.headers.range;

                    if (range) {
                        const parts = range.replace(/bytes=/, "").split("-");
                        const start = parseInt(parts[0], 10);
                        const end = parts[1] ? parseInt(parts[1], 10) : stat.size - 1;

                        if (start >= stat.size) {
                            res.status(416).send('Requested range not satisfiable\n' + start + ' >= ' + stat.size);
                            return;
                        }

                        res.writeHead(206, {
                            'Content-Range': `bytes ${start}-${end}/${stat.size}`,
                            'Accept-Ranges': 'bytes',
                            'Content-Length': (end - start) + 1,
                            'Content-Type': 'video/mp4',
                        });
                        fs.createReadStream(file_path, {
                            start,
                            end
                        }).pipe(res);
                    } else {
                        res.writeHead(200, {
                            'Content-Length': stat.size,
                            'Content-Type': 'video/mp4',
                        });
                        fs.createReadStream(file_path).pipe(res);
                    }
                }).catch(function(error) {
                    res.locals.log.general.error(error);
                    res.status(400).send("File not found");
                });
            }]
        }
    }
};