const path = require("path");
const fs = require("fs");
const express = require("express");
const mime = require("mime-types");
const HttpStatus = require('http-status-codes');
const dataManager = require("./dataManager");
const imageWriter = require("./image/imageWriter");
const ANIME_FOLDER = "episode";
const JSON_ANIME = "index.json";
let JsonObject = dataManager.JsonObject;
let DownloadEpisode = dataManager.DownloadEpisode;
let VideoPlayer = dataManager.VideoPlayer;
let YoutubePlayer = dataManager.YoutubePlayer;
let Anime = dataManager.Anime;
let Episode = dataManager.Episode;
/**
* Start the server
*
* ------------------
* Server links :
* [USE] `*` - Used to do consoleGroup
* <br/><br/>
* [GET] `/new/anime?` - Add a new anime
*    Param : `name` - The name of the anime
*    Param : `thumbnailLink` - The path to the thumbnail
*    Send : {"id":Anime Id}
* <br/>
* [GET] `/new/episode?` - Add a new episode to an anime
*    Send : {"id":Anime Id}
* <br/>
* [GET] `/get/list` - Get the anime list
*    Send : {@link Anime.publicList Anime.publicList}
* <br/>
* [GET] `/get/episode/info?` - Get the info of an episode
*    Param : `animeId` - The id of the anime
*    Param : `episodeId` - The id of the episode in the anime
*    Send : {@link EpisodeInfo}
* <br/>
* [GET] `/get/episode/download?` - Download an episode
*    Param : `animeId`
*    Param : `episodeId`
*    Param : `videoPlayerId`
*    Param : `format`
*    Param : `url`
*    Send : {"progress":{@link DownloadEpisode#progress DownloadEpisode.progress}}
* <br/>
* [GET] `/get/list/download` - Get download list
* <code class="prettyprint source language-json code-toolbar pre">
* Send: {
*    "current": {
*       "progress":{@link DownloadEpisode#progress DownloadEpisode.progress}
*       "episode": Episode's name
*    },
*    "list": [
*       Episodes's name
*    ],
*    "error": [
*       {
*          "episode": Episode's name
*          "error": Error
*       }
*    ]
* }
* </code>
* <br/>
* [GET] `/` - Index.html
* <br/>
* [GET] `/js/*` - Folder js
* <br/>
* [GET] `/html/*` - Folder html
* <br/>
* [GET] `/css/*` - Folder css
* <br/>
* [GET] `/fonts/*` - Folder fonts
* <br/>
* [GET] `/asset/ico/*` - Folder assets ico
* <br/>
* [GET] `/episode/:animeId/:episodeId` - Get a local episode in folder episode
*    Param : `animeId` - The id of the anime
*    Param : `episodeId` - The id of the episode in the anime
* <br/>
* [GET] `/asset/thumbnail/:text.png` - Generate an image or return a generated image
*    Param : `text` - The text to show on the image
*    Param : `width` - The image width
*    Param : `height` - The image height
*    Param : `textSize` - The text size
*    Param : `backgroundColor` - The background color
*    Param : `textColor` - The text color
* @protected
* @memberof server.https
* @param {Config} config
*/
function start(config) {
/**
* @constant
*/
let app = express();
const cors = require('cors');
app.use(cors());
/**
* @type {number}
*/
let port = config.port;
app.use('*', (req,res,next) =>
{
console.group(`[${req.method}] `+(req.baseUrl || "/"));
next();
res.once("close", () => {
console.groupEnd();
});
});
//*///////////////////////////////*//
//* New Anime *//
//*///////////////////////////////*//
app.get('/new/anime', (req, res, next) => {
let animeName = req.query.name;
let animeThumbnailLink = req.query.thumbnailLink;
/**
* @ignore
* @type {import("./dataManager").AnimeConfig}
*/
let animeData = {"name":animeName, "episodes":[]}
if (animeThumbnailLink)
{
animeData.thumbnailLink = animeThumbnailLink;
}
let animeFolderName = global.toFileName(req.name.animeName, "-").replace(/\ /g,"_");
let animeFolder = path.join(__root, ANIME_FOLDER, animeFolderName);
let jsonFile = path.join(animeFolderName, JSON_ANIME);
fs.mkdir(animeFolderName, () => {
fs.writeFile(jsonFile, JSON.stringify(animeData), () => {
let jsonObj = new JsonObject(jsonFile);
jsonObj.value = animeData;
let anime = new Anime(jsonObj, animeFolder);
res.send(JSON.stringify({'id': anime.id}));
});
});
});
//*///////////////////////////////*//
//* New Episode *//
//*///////////////////////////////*//
app.get('/new/episode', (req, res, next) => {
let animeId = req.query.animeId;
let episodeName = req.query.name;
let posterLink = req.query.posterLink;
let anime = tryToGetAnimeOrSendStatus(animeId)
if (!anime) return;
/**
* @type {import("./dataManager").EpisodeConfig}
*/
let epConfig = {"episodeId":anime.episodes.length,"links":[],"name":episodeName,"posterLink":posterLink};
let ep = new Episode(epConfig, anime);
anime.episodes.push(ep);
res.send(JSON.stringify({'id': ep.episodeId}));
});
//*///////////////////////////////*//
//* Get List *//
//*///////////////////////////////*//
app.get('/get/list', (req, res, next) => {
res.contentType("application/json");
res.send(JSON.stringify(Anime.publicList));
});
//*///////////////////////////////*//
//* Get Episode Info *//
//*///////////////////////////////*//
function tryToGetAnimeOrSendStatus(res, animeId)
{
//Arguments checking
if (!Number.isSafeInteger(animeId)) {
console.log("[Missing Argument]");
res.sendStatus(HttpStatus.BAD_REQUEST);
return;
}
//Anime finding
let lAnime = Anime.list[animeId]
if (!lAnime) {
console.log("[Anime not found]");
res.sendStatus(HttpStatus.NOT_FOUND);
return;
}
return lAnime;
}
function tryToGetEpisodeOrSendStatus(res, animeId, episodeId)
{
//Arguments checking
if (!Number.isSafeInteger(animeId) || !Number.isSafeInteger(episodeId)) {
console.log("[Missing Argument(s)]");
res.sendStatus(HttpStatus.BAD_REQUEST);
return;
}
//Anime finding
let lAnime = Anime.list[animeId]
if (!lAnime) {
console.log("[Anime not found]");
res.sendStatus(HttpStatus.NOT_FOUND);
return;
}
//Episode finding
let lEpisode = lAnime.getEpisodeById(episodeId);
if (!lEpisode) {
console.log("[Episode not found]");
res.sendStatus(HttpStatus.NOT_FOUND);
return;
}
return lEpisode
}
app.get('/get/episode/info', async (req, res, next) => {
let animeId = Number.parseInt(req.query.animeId);
let episodeId = Number.parseInt(req.query.episodeId);
let loadYtInfo = req.query.loadYtInfo;
try {
loadYtInfo = JSON.parse(loadYtInfo);
if (typeof(loadYtInfo) !== "boolean")
{
loadYtInfo = true;
}
}
catch(e)
{
loadYtInfo = true;
}
console.log("?animeId = "+animeId);
console.log("?episodeId = "+episodeId);
console.log("?loadYtInfo = "+loadYtInfo);
let lEpisode = tryToGetEpisodeOrSendStatus(res, animeId, episodeId);
if (!lEpisode) return;
//Get episode info and send
try {
let info = await lEpisode.getInfo(loadYtInfo);
res.contentType("application/json");
res.send(JSON.stringify(info));
}
catch(e)
{
console.log(e);
res.sendStatus(HttpStatus.INTERNAL_SERVER_ERROR);
}
});
//*///////////////////////////////*//
//* Download Episode *//
//*///////////////////////////////*//
app.get('/get/episode/download', async (req, res, next) => {
let animeId;
let episodeId;
let videoPlayerId;
let format;
let url;
try {
animeId = Number.parseInt(req.query.animeId);
episodeId = Number.parseInt(req.query.episodeId);
videoPlayerId = Number.parseInt(req.query.videoPlayerId);
format = req.query.format ? JSON.parse(req.query.format) : null;
url = req.query.url;
}
catch(e)
{
console.error(e);
res.sendStatus(HttpStatus.BAD_REQUEST);
return;
}
console.log("?animeId = "+animeId);
console.log("?episodeId = "+episodeId);
console.log("?videoPlayerId = "+videoPlayerId);
console.log("?ytFormat = "+JSON.stringify(format));
console.log("?url = "+url);
let lEpisode = tryToGetEpisodeOrSendStatus(res, animeId, episodeId);
if (lEpisode.isLocal)
{
//console.log("[Redirect] "+req.hostname+`/episode/${animeId}/${episodeid}`);
//res.redirect(req.hostname+`/episode/${animeId}/${episodeid}`);
res.sendStatus(HttpStatus.CONFLICT);
return;
}
let download = DownloadEpisode.getFromEpisode(lEpisode) || new DownloadEpisode(lEpisode, videoPlayerId);
if (download.isError)
{
download.destroy();
download = new DownloadEpisode(lEpisode, videoPlayerId);
}
if (!download.isDownloading && !download.isPending) download.download(url, format);
//Episode
res.send(JSON.stringify({progress:(download.progress || 0)}));
});
//*///////////////////////////////*//
//* Download List *//
//*///////////////////////////////*//
app.get('/get/list/download', async (req, res, next) => {
let downloads = DownloadEpisode.toDownload;
let errorList = DownloadEpisode.list.filter(e => e.isError);
let current = {}
if (DownloadEpisode.currentDownload)
{
let currentDlEpisode = DownloadEpisode.currentDownload.episode;
current = {
progress: DownloadEpisode.currentDownload.progress,
episode: currentDlEpisode.anime.name + " : " + (currentDlEpisode.name || currentDlEpisode.episodeId)
}
}
//Episode
res.send(JSON.stringify({
current: current,
list : downloads.map( (d) => {
let e = d.downloadEpisode.episode;
return e.anime.name + " : " + (e.name || e.episodeId);
}),
error : errorList.map( (d) => {
let e = d.episode;
return {episode: e.anime.name + " : " + (e.name || e.episodeId), error : d.error};
})
}));
});
function loadAndSendFile(req,res,filePath)
{
console.log("[File path] "+filePath);
res.sendFile(filePath), (err) => {
if (err) console.error(err);
};
}
//*///////////////////////////////*//
//* Index.html *//
//*///////////////////////////////*//
app.get('/', (req, res, next) => {
loadAndSendFile(req, res, path.join(__root,"public","index.html"));
});
//*///////////////////////////////*//
//* Public Folder *//
//*///////////////////////////////*//
let folders = ["js","html","css", "fonts", "asset/ico"];
folders = folders.map(m => {return `/${m}/*`});
app.get(folders, (req, res, next) => {
loadAndSendFile(req, res, path.join(__root,"public",req.path));
});
//*///////////////////////////////*//
//* Get Local Episode *//
//*///////////////////////////////*//
app.get("/episode/:animeId/:episodeId", (req, res, next) => {
let animeId = Number.parseInt(req.params.animeId);
let episodeId = Number.parseInt(req.params.episodeId);
console.log(":animeId = "+animeId);
console.log(":episodeId = "+episodeId);
let episode = tryToGetEpisodeOrSendStatus(res, animeId, episodeId);
loadAndSendFile(req, res, episode.path);
});
//*///////////////////////////////*//
//* Thumbnail *//
//*///////////////////////////////*//
app.get("/asset/thumbnail/:text.png", async (req, res, next) => {
const commands = require("./image/imageWorkerCommands.js");
let fileExtension = commands.fileExtension;
let width = Number.parseInt (req.query.width);
let height = Number.parseInt (req.query.height);
let textSize = Number.parseFloat (req.query.textSize);
let backgroundColor = req.query.backgroundColor;
let textColor = req.query.textColor;
let text = req.params.text;
let thumbailsFolder = path.join(__root,"public/asset/thumbails");
let pathToTest = path.join(thumbailsFolder, `${text}.${fileExtension}`);
if (!fs.existsSync(thumbailsFolder))
{
fs.mkdirSync(thumbailsFolder);
}
//If keep thumbnails, send them
if (config.keepThumbnails)
{
let thumbnlailExists = fs.existsSync(pathToTest);
if (thumbnlailExists)
{
return loadAndSendFile(req, res, pathToTest);
}
}
/**
* @type {imageWriter.ThumbailOption}
*/
let options = {};
if (Number.isSafeInteger(width)) options.width = width;
if (Number.isSafeInteger(height)) options.height = height;
if (!Number.isNaN(textSize)) options.textSize = textSize;
if (backgroundColor) options.backgroundColor = backgroundColor;
if (textColor) options.textColor = textColor;
if (width > 2000 || width <= 0) options.width = null;
if (height > 2000 || height <= 0) options.height = null;
/**
* @ignore
* @type {string}
*/
let filePath;
try {
filePath = await imageWriter.getThumbail(text, options)
}
catch(e)
{
console.log(e);
res.sendStatus(HttpStatus.INTERNAL_SERVER_ERROR);
return;
}
//This code is executed on succes
/////////////////////////////////
res.sendFile(filePath, (e) => {
if (e) {
console.error(e);
return;
}
if (config.keepThumbnails)
{
//Keep thumbnail
let renamePath = pathToTest;
fs.rename(filePath, renamePath, () => {
console.log(`Saved thumbnail as \"${renamePath}\"`)
});
}
else
{
//Remove thumbnail
console.log(`Removing temp file at path \"${filePath}\"...`);
fs.unlink(filePath, (err) => {
console.log(`Removed temp file`);
if (err) console.error(err);
});
}
});
});
app.listen(port, function () {
console.log(`App listening on port ${port}!`);
});
};
module.exports = start;