/**
* The config of a videoplayer
* @memberof server.data.config
* @typedef {Object} VideoPlayerConfig
* @property {string} name
* @property {string[]} prefix
* @property {boolean} downloadable
* @property {boolean} autoDownload
* @property {boolean} isNatif
* @property {boolean} isYoutube
*/
/**
* The public informations of a video player
* @memberof server.data.public
* @typedef {Object} PublicVideoPlayer
* @property {string} name
* @property {boolean} isNatif
* @property {boolean} downloadable
* @property {boolean} autoDownload
* @property {number} id
*/
/**
* The info of an URL
* @memberof server.data.public
* @typedef {Object} PlayerInfo
* @property {string} url
* @property {string} [ytInfo]
* @property {boolean} [isYoutube]
* @property {PublicVideoPlayer} player
*/
/*-****************************-*/
/**
* The config of and anime
* @memberof server.data.config
* @typedef {Object} AnimeConfig
* @property {string} name
* @property {string} [thumbnailLink]
* @property {EpisodeConfig[]} episodes
*/
/**
* The public informations of an anime
* @memberof server.data.public
* @typedef {Object} PublicAnime
* @property {number} id
* @property {PublicEpisode} episodes
* @property {string} name
* @property {string} thumbnailLink
*/
/*-*****************************-*/
/**
* The config of an episode
* @memberof server.data.config
* @typedef {Object} EpisodeConfig
* @property {string} [name]
* @property {number} episodeId
* @property {string} [posterLink]
* @property {string[]} links
* @property {string} [localLink]
*/
/**
* The public informations of an episode
* @memberof server.data.public
* @typedef {Object} PublicEpisode
* @property {string} name The name of the episode
* @property {number} animeId The unique id of the anime
* @property {number} episodeId The unique id of the episode (define the order between episodes)
* @property {string} posterLink The uri of the anime poster
*/
/**
* The information of an episode (urls are mapped with {@link PlayerInfo PlayerInfo})
* @memberof server.data.public
* @typedef {Object} EpisodeInfo
* @property {string} name
* @property {number} episodeId
* @property {string} posterLink
* @property {bool} isLocal
* @property {bool} hasPoster
* @property {PlayerInfo[]} players
*/
/**
* The data of a download
* @memberof data
* @typedef {Object} ReqDownloadData
* @property {number} progress The download progress in %
* @property {string} contentType
* @property {string} fileName
*/
/** */
const http = require('http');
const https = require('https');
var fs = require('fs');
var ytdl = require('ytdl-core');
const pathNode = require("path");
const mime = require("mime-types");
const event = require("events");
const { error } = require('console');
const EventEmitter = event.EventEmitter;
/**
* Load and save a json file
* @public
* @memberof server
*/
class JsonObject {
/**
* @public
* @param {string} path
*/
constructor(path)
{
this.path = path;
this.value = {};
}
/**
* @public
* @param {fs.NoParamCallback} [func]
*/
loadSync(func)
{
console.newLine();
console.log("Loading sync "+this.path);
var data = fs.readFileSync(this.path);
try {
this.value = JSON.parse(data);
console.dir(myObj);
}
catch (err) {
console.log(`There has been an error parsing your file "${this.path}".`)
console.log(err);
if (func) func(err);
}
}
/**
* @public
*/
load()
{
console.newLine();
console.log("Loading "+this.path);
return new Promise((resolve, reject) => {
fs.readFile(this.path, (err, data) => {
var error = err;
if (!error) {
try {
this.value = JSON.parse(data);
}
catch (err) {
console.log(`There has been an error parsing your file "${this.path}".`)
console.log(err);
error = err;
}
}
if (error) reject(error);
resolve();
});
});
}
/**
* @public
* @returns {Promise<void>}
*/
save()
{
console.newLine();
console.log("Saving "+this.path);
return new Promise((resolve, reject) => {
var data = JSON.stringify(this.value, "", "\t");//add tabs to make it more readable
fs.writeFile(this.path, data, (err) => {
if (err) {
console.log(`There has been an error saving your file "${this.path}".`);
console.log(err.message);
reject(err);
return;
}
console.log("Saved "+this.path);
resolve();
});
});
}
}
/**
* A class used to handle the events of {@link VideoPlayer#download VideoPlayer.download}.
* Ensure that there's only one download max by episode
* @public
* @memberof server
*/
class DownloadEpisode
{
/**
* @public
* @readonly
* @type {DownloadEpisode[]}
*/
static get list() {return DownloadEpisode._list || (DownloadEpisode._list = [])}
/**
* @typedef ToDownloadItem
* @property {Function} func
* @property {DownloadEpisode} downloadEpisode
* @memberof DownloadEpisode
* @public
*/
/**
* @public
* @type {ToDownloadItem[]}
*/
static get toDownload() {return DownloadEpisode._toDownload || (DownloadEpisode._toDownload = [])}
/**
* @public get
* @protected set
* @type {DownloadEpisode}
*/
static get currentDownload() {return DownloadEpisode._currentDownload || null}
static set currentDownload(value) {return DownloadEpisode._currentDownload = value}
/**
* Constructor of the class
* @param {Episode} episode
* @param {number} videoPlayerId
* @see {@link Videoplayer#id}
*/
constructor(episode, videoPlayerId)
{
/**
* The episode we want to download
* @public
* @readonly
* @type {Episode}
*/
this.episode = episode;
/**
* The unique id of the DownloadEpisode instance
* @public
* @readonly
* @type {number}
*/
this.id = DownloadEpisode.list.length;
/**
* The player used to download the video (epsode)
* @private
* @readonly
* @type {VideoPlayer}
*/
this.player = VideoPlayer.getVideoPlayerById(videoPlayerId);
/**
* If the download is done and the json saved
* @private
* @readonly
* @type {boolean}
* @see {@link DownloadEpisode#download}, {@link DownloadEpisode#_setLocalPath}
*/
this.isReady = false;
/**
* If the download is pending
* @public
* @readonly
* @type {boolean}
*/
this.isPending = false;
/**
* If the download is ongoing
* @public
* @readonly
* @type {boolean}
*/
this.isDownloading = false;
/**
* If there is an error
* @public
* @readonly
* @type {boolean}
*/
this.isError = false;
/**
* The error
* @public
* @readonly
* @type {string}
*/
this.error = "";
/**
* The progress of the download
* @public
* @readonly
* @type {number}
*/
this.progress = 0;
if (!this.player.downloadable) throw "The episode is not downloadable";
DownloadEpisode.list.push(this);
}
/**
* Call {@link Episode#setLocalPath} and set itself ready when it's done
* @private
* @param {string} pathToFile
* @see {@link Episode#setLocalPath}
*/
_setLocalPath(pathToFile)
{
this.episode.setLocalPath(pathToFile.replace(/^.*(\\|\/)episode(\\|\/).*(\\|\/)/, ""))
.then(
() => {
this.isReady = true;
this.isDownloading = false;
this.progress = 1;
}
);
}
/**
* Launch the download
* @param {string} url
* @param {object} format
* @see {@link DownloadEpisode#_setEvents}
*/
download(url, format)
{
if (this.isDownloading)
{
console.error(`${nameof({DownloadEpisode})} is downloading`);
}
if (DownloadEpisode.currentDownload === null)
{
DownloadEpisode.toDownload.shift();
this.isPending = false;
this.isDownloading = true;
DownloadEpisode.currentDownload = this;
let emitter = this.player.download((this.player.autoDownload ? this.episode.getUrlByPlayer(this.player) : url ) , format, this.episode.path);
if (emitter == null)
{
DownloadEpisode.currentDownload = null;
this.isDownloading = false;
if (DownloadEpisode.toDownload.length > 0) DownloadEpisode.toDownload[0].func();
return;
}
this._setEvents(emitter);
}
else if (!this.isPending)
{
this.isPending = true;
DownloadEpisode.toDownload.push(
{
func : this.download.bind(this, url, format),
downloadEpisode : this
}
);
}
}
/**
* Set the events of the emitter
* @private
* @param {event.EventEmitter} emitter
* @see {@link DownloadEpisode#download}
*/
_setEvents(emitter)
{
emitter
.on('progress',
/**
* @param {ReqDownloadData} recDownloadData
*/
(recDownloadData) => {
this.progress = recDownloadData.progress / 100 * 0.99;
})
.on('complete',
/**
* @param {ReqDownloadData} recDownloadData
*/
(recDownloadData) => {
this.progress = 0.99;
this._setLocalPath(recDownloadData.fileName);
DownloadEpisode.currentDownload = null;
this.destroy();
if (DownloadEpisode.toDownload.length > 0) DownloadEpisode.toDownload[0].func();
})
.on('error',
/**
* @param {string} err
*/
(err) => {
DownloadEpisode.currentDownload = null;
this.isDownloading = false;
this.isError = true;
this.error = err;
console.error(err);
if (DownloadEpisode.toDownload.length > 0) DownloadEpisode.toDownload[0].func();
});
}
/**
* Destroy the instance (Removes it from {@link DownloadEpisode#list DownloadEpisode.list}.)
* @public
*/
destroy()
{
DownloadEpisode.list.splice( DownloadEpisode.list.indexOf(this), 1);
}
/**
*
* @param {Episode} episode
*/
static getFromEpisode(episode)
{
for (let i = DownloadEpisode.list.length - 1; i >= 0; i--) {
let lElement = DownloadEpisode.list[i];
if (lElement.episode == episode) return lElement;
}
return null;
}
}
/**
* A class used to download a video.
* @public
* @memberof server
*/
class VideoPlayer {
/**
* The list of VideoPlayer
* @public
* @readonly
* @type {VideoPlayer[]}
*/
static get list() {return VideoPlayer._list || (VideoPlayer._list = []);}
/**
* Constructor of the class
* @public
* @param {VideoPlayerConfig} config
*/
constructor(config)
{
/**
* The name of the videoPlayer
* @public
* @readonly
* @type {string}
*/
this.name = config.name
/**
* If true, the VideoPlayer wront be available for edit
* @public
* @readonly
* @type {boolean}
*/
this.isNatif = config.isNatif;
/**
* The prefix(s) for matching the url
* @public
* @type {string[]}
* @see {@link Videoplayer#getPlayer}, {@link Videoplayer#hasPrefix}
*/
this.prefix = config.prefix;
/**
* Tell if the server can download
* @public
* @type {boolean}
*/
this.downloadable = config.downloadable;
/**
* Tell if the url used for downloading is an url in {@link Episode#links Episode.links}
* @public
* @type {boolean}
*/
this.autoDownload = config.autoDownload;
/**
* The unique id of the videoPlayer
* @public
* @readonly
*/
this.id = VideoPlayer.list.length;
VideoPlayer.list.push(this);
}
/**
* Return the public information of the VideoPlayer (= the informations to give to the client)
* @public
* @returns {PublicVideoPlayer}
*/
toPublic()
{
return {
name: this.name,
isNatif: this.isNatif,
downloadable: this.downloadable,
autoDownload: this.autoDownload,
id: this.id
};
}
/**
* Fire the event "complete" on the emitter
* @protected
* @param {event.EventEmitter} emitter
* @param {ReqDownloadData} recDownloadData
*/
_dispatchOnComplete(emitter, recDownloadData)
{
/**
* @event complete
* @type {ReqDownloadData}
*/
emitter.emit('complete', recDownloadData);
}
/**
* Fire the event "progress" on the emitter
* @protected
* @param {event.EventEmitter} emitter
* @param {ReqDownloadData} recDownloadData
*/
_dispatchOnProgress(emitter, recDownloadData)
{
/**
* @event progress
* @type {ReqDownloadData}
*/
emitter.emit('progress', recDownloadData);
}
/**
* Fire the event "error" on the emitter
* @protected
* @fires error
* @param {event.EventEmitter} emitter
* @param {string} err
*/
_dispatchOnError(emitter, err)
{
/**
* @event error
* @type {string}
*/
emitter.emit('error', err);
}
/**
* Download the video using the given link (async)
* @public
* @fires progress
* @fires complete
* @fires error
* @param {string} downloadUrl
* @param {object} format unused
* @param {string} fileName
* @returns {EventEmitter}
*/
download(downloadUrl, format, fileName)
{
let emitter = new EventEmitter();
try {
var url = new URL(downloadUrl);
}
catch(_)
{
return null;
}
/**
* @type {http.RequestOptions}
*/
let options = {
headers: {Accept: "video/webm, video/mpeg, video/ogg"},
timeout: 30000,
}
/**
* @type {fs.WriteStream}
*/
let file = null;
/**
* @type {http.ClientRequest || https.ClientRequest}
*/
let request = null;
switch (url.protocol) {
case "http:":
request = http.get(downloadUrl, options);
break;
case "https:":
request = https.get(downloadUrl, options);
break;
default:
setTimeout( () => {
this._dispatchOnError(emitter, "Protocol not supported");
}, 1000 );
return;
}
request.setTimeout(30000, async () => {
request.abort();
file.close();
// Delete the file async. (But we don't check the result)
fs.unlink(fileName, () => {
this._dispatchOnError(emitter, "[Request Timeout] "+fileName);
});
});
request.on("response", async (response) => {
let len = parseInt(response.headers['content-length'], 10);
let downloaded = 0;
let contentType = response.headers["content-type"];
//Init the file with the response extension
if (file === null) {
fileName = `${fileName}.${mime.extension(contentType)}`;
file = fs.createWriteStream(fileName);
}
response.on('data', (chunk) => {
//Write chunk into the file
file.write(chunk);
//Get progress
downloaded += chunk.length
let progress = (100.0 * downloaded / len).toFixed(2)
//process.stdout.write(`Downloading ${percent}% ${downloaded} bytes\r`)
this._dispatchOnProgress(emitter, {progress, contentType, fileName});
})
response.on('end', async () => {
// close() is async, call resolve after close completes.
file.close();
if (!response.complete)
{
let err = 'The connection was terminated while the message was still being sent';
console.error(err);
//this._dispatchOnError(emitter, err);
return;
}
this._dispatchOnComplete(emitter, {progress:100, contentType, fileName});
});
})
// Handle errors
.on('error', async (err) => {
file.close();
// Delete the file async. (But we don't check the result)
fs.unlink(fileName, () => {
this._dispatchOnError(emitter, err.message);
});
});
return emitter;
}
/**
* Compare the url with the prefix and return true if the url match a prefix in the prefix list
* @public
* @param {string} url
* @returns {boolean} Return true if the url match a prefix in the prefix list
* @see {@link Videoplayer.prefix}
*/
hasPrefix(url)
{
for (let i = this.prefix.length - 1; i >= 0; i--) {
let lElement = this.prefix[i];
if (url.startsWith(lElement)) return true;
}
return false;
}
/**
* Return the first Videoplayer with its prefix matching the url
* @public
* @param {string} url
* @returns {VideoPlayer}
* @see {@link Videoplayer.prefix}
*/
static getPlayer(url)
{
for (let i = VideoPlayer.list.length - 1; i >= 0; i--) {
let lElement = VideoPlayer.list[i];
if (lElement.hasPrefix(url)) return lElement;
}
return null;
}
/**
* Return the video player corrisponding
* @public
* @param {number} id
* @return {VideoPlayer}
*/
static getVideoPlayerById(id)
{
return VideoPlayer.list[id];
}
}
/**
* A class used to download a youtube video. It also get the video info.
* @public
* @extends VideoPlayer
* @memberof server
*/
class YoutubePlayer extends VideoPlayer {
/**
* Unique instance of the class
* @public
* @readonly
* @returns {YoutubePlayer}
*/
static get instance() {return YoutubePlayer._instance}
/**
* Constructor of the class
* @public
* @param {VideoPlayerConfig} config
*/
constructor(config)
{
super(config);
if (!YoutubePlayer._instance) YoutubePlayer._instance = this;
else console.warn("2 YoutubePlayer has been founded");
}
/**
* Download the youtube video (async)
* @public
* @override
* @fires progress
* @fires complete
* @fires error
* @param {string} downloadUrl Unused
* @param {ytdl.videoFormat} format
* @param {string} localFileWithoutExtension
* @returns {EventEmitter}
*/
download(downloadUrl, format, localFileWithoutExtension)
{
let emitter = new EventEmitter();
let extension = mime.extension(format.mimeType) || "";
let path = localFileWithoutExtension+"."+extension;
let video = ytdl(downloadUrl, {format: format} );
video.on('response', (res) => {
var totalSize = res.headers['content-length'];
var dataRead = 0;
res.on('data', (data) => {
dataRead += data.length;
var percent = dataRead / totalSize;
let progress = (percent * 100).toFixed(2);
this._dispatchOnProgress(emitter, { progress, contentType:format.mimeType, fileName: path } );
});
})
.on('error', (e) => {this._dispatchOnError(emitter, e);});
let stream = video.pipe(fs.createWriteStream(path));
stream.on('finish', async () => {
await stream.close();
this._dispatchOnComplete(emitter, { progress: 100, contentType:format.mimeType, fileName: path });
});
return emitter;
}
/**
* Fetch the informations of the youtube video
* @public
* @requires "node_modules/ytdl"
* @param {string} url
* @returns {Promise<ytdl.videoInfo>}
* @see ytdl#getInfo
*/
getInfo(url)
{
return new Promise((resolve, reject) => {
ytdl.getInfo(url, {filter: "audioandvideo"})
.then(info => {
resolve(info);
})
.catch(err => {
console.error(`Error loading \"${url}\"`);
console.error(err);
reject(err);
return;
});
});
}
}
/**
* Store the datas of an anime. Can update the index.json of the anime.
* @public
* @memberof server
*/
class Anime {
/**
* The list of Animes
* @public
* @readonly
* @type {Anime[]}
*/
static get list() {return Anime._list || (Anime._list = []);}
/**
* The list of Animes mapped with their public informations
* @public
* @returns {PublicAnime[]}
*/
static get publicList()
{
return Anime.list.map(m => m.toPublic());
}
/**
* Return the public information of the anime (= the informations to give to the client)
* @public
* @returns {PublicAnime}
*/
toPublic() {
/**
* @type {PublicAnime}
*/
var lToReturn = {
id : this.id,
episodes : this.episodes.map(e => e.toPublic()),
name : this.name,
thumbnailLink : this.thumbnailLink
};
return lToReturn;
}
/**
* Constructor of the class
* @public
* @param {JsonObject} jsonObject
* @param {string} folderPath
*/
constructor(jsonObject, folderPath)
{
/**
* @ignore
* @type {AnimeConfig}
*/
let data = jsonObject.value;
/**
* The json object of the anime. Used to store the datas in the index.json of the anime.
* @private
* @readonly
* @type {JsonObject}
*/
this.jsonObject = jsonObject;
/**
* The name of the anime
* @public
* @type {string}
*/
this.name = data.name;
if (!data.name) throw `"${nameof(name)}" is null in anime : `+folderPath;
/**
* The uri of the anime poster
* @public
* @type {string}
*/
this.thumbnailLink = data.thumbnailLink;
/**
* The path to the anime's folder
* @private
* @readonly
* @type {string}
*/
this._path = folderPath;
if (!folderPath) throw `${nameof(folderPath)} is null (code exception)`;
/**
* The list of episodes
* @public
* @type {Episode[]}
*/
this.episodes = [];
let episodes = data.episodes;
if (!episodes) throw `"${nameof(episodes)}" is null in anime : `+folderPath;
for (let i = episodes.length - 1; i >= 0; i--) {
let lElement = episodes[i];
try
{
this.episodes.push(new Episode(lElement, this));
}
catch(e)
{
console.error(e);
}
}
this.episodes = this.episodes.sort( (a,b) => a.episodeId - b.episodeId);
/**
* The unique id of the anime
* @public
* @readonly
* @type {number}
*/
this.id = Anime.list.length;
Anime.list.push(this);
}
/**
* Get an anime's {@link Episode} by its id
* @public
* @param {number} episodeId
* @returns {(Episode|null)}
* @see {Episode#episodeId}
*/
getEpisodeById(episodeId)
{
for (let i = this.episodes.length - 1; i >= 0; i--) {
let lElement = this.episodes[i];
if (lElement.episodeId == episodeId) return lElement;
}
return null;
}
/**
* Update the json by generating the {@link AnimeConfig} and getting the {@link EpisodeConfig} of all its episode
* @public
* @returns {Promise<void>}
* @see {@link JsonObject}
*/
updateJson()
{
this.jsonObject.value = this.toAnimeConfig();
return this.jsonObject.save().catch( () => {console.error(`Can't save anime ${this.name}`);});
}
/**
* Return the {@link AnimeConfig} of the Anime
* @public
* @returns {AnimeConfig}
*/
toAnimeConfig()
{
/**
* @type {AnimeConfig}
*/
let animeConfig = {
episodes: this.episodes.map((m) => {return m.toEpisodeConfig()})
};
if(this.name) animeConfig.name = this.name;
if(this.thumbnailLink) animeConfig.thumbnailLink = this.thumbnailLink;
return animeConfig;
}
/**
* Return the path to the anime folder
* @public
* @readonly
* @returns {string}
*/
get path()
{
return this._path;
}
}
/**
* Store the datas of an episode
* @public
* @memberof server
*/
class Episode {
/**
* Constructor of the class
* @public
* @param {EpisodeConfig} config
* @param {Anime} anime
*/
constructor(config, anime)
{
if (config.episodeId === null || config.episodeId === undefined) throw `"${nameof({episodeId})}" is null in anime : `+folderPath;
/**
* The name of the episode
* @public
* @type {string}
*/
this.name = config.name || "";
/**
* The unique id of the episode (define the order between episodes)
* @public
* @readonly
* @type {number}
*/
this.episodeId = config.episodeId || -1;
/**
* The uri of the episode poster
* @public
* @type {string}
*/
this.posterLink = config.posterLink || "";
/**
* The episode's stream links
* @public
* @type {string[]}
*/
this.links = config.links;
/**
* The local path to episode's file.
* To set {@link Episode#localLink Episode.localLink}, see : {@link Episode#setLocalPath Episode.setLocalPath}
* @public
* @readonly
* @type {string}
*/
this.localLink = config.localLink || "";
/**
* Reference to the {@link Anime}
* @public
* @readonly
* @type {Anime}
*/
this.anime = anime;
}
/**
* Return the public information of the episode (= the informations to give to the client)
* @public
* @returns {PublicEpisode}
*/
toPublic() {
/**
* @type {PublicEpisode}
*/
var lToReturn = {
name : this.name,
animeId : this.anime.id,
episodeId : this.episodeId,
posterLink : this.posterLink
};
return lToReturn;
}
/**
* Get the info of an episode
* @public
* @param {bool} loadYoutubeInfo Decide or not to call {@link YoutubePlayer#getInfo YoutubePlayer.getInfo}
* @returns {Promise<EpisodeInfo>}
*/
async getInfo(loadYoutubeInfo = true)
{
/**
* @type {EpisodeInfo}
*/
let lToReturn = this.toPublic();
lToReturn.players = [];
lToReturn.isLocal = this.isLocal;
lToReturn.hasPoster = this.hasPoster;
for (let i = this.links.length - 1; i >= 0; i--) {
/**
* @type {PlayerInfo}
*/
let lToPush = {};
let url = this.links[i];
let videoPlayer = VideoPlayer.getPlayer(url);
if (!videoPlayer) continue;
lToPush.isYoutube = false;
if (videoPlayer instanceof YoutubePlayer)
{
lToPush.isYoutube = true;
if (loadYoutubeInfo) {
try {
let ytInfo = await videoPlayer.getInfo(url);
lToPush.ytInfo = ytInfo;
} catch (e)
{
lToPush.ytInfo = {formats : []};
}
}
}
lToPush.url = url;
lToPush.player = videoPlayer.toPublic();
lToReturn.players.push(lToPush);
}
return lToReturn;
}
/**
* Get the first url corresponding to the {@link Videoplayer}
* @public
* @param {VideoPlayer} player
* @returns {string} Return the first url corresponding to the player
*/
getUrlByPlayer(player)
{
for (let i = this.links.length - 1; i >= 0; i--) {
let url = this.links[i];
if (player.hasPrefix(url))
return url;
}
return "";
}
/**
* Set {@link Episode#localLink Episode.localLink} and update the anime
* @public
* @param {string} path
* @returns {Promise<void>}
* @see {@link Anime#updateJson Anime.updateJson}
*/
setLocalPath(path)
{
this.localLink = path;
return this.anime.updateJson();
}
/**
* True when ${@link Episode#localLink localLink} is set
* @public
* @type {boolean}
*/
get isLocal() {return Boolean(this.localLink);}
/**
* True when ${@link Episode#posterLink posterLink} is set
* @public
* @type {boolean}
*/
get hasPoster() {return Boolean(this.posterLink);}
/**
* path.join(${@link Anime#path this.anime.path} with :
*
* If there is no local file, use the default local path ${@link Episode#episodeId this.episodeId})`
* Else use the ${@link Episode#localLink local path}
* @public
* @type {string}
*/
get path()
{
let animePath = this.anime.path;
return pathNode.join(animePath, (this.isLocal ? this.localLink : `ep${this.episodeId}`));
}
/**
* Return the {@link EpisodeConfig} of the Episode
* @public
* @returns {EpisodeConfig}
*/
toEpisodeConfig()
{
/**
* @type {EpisodeConfig}
*/
let config = {};
if(this.name) config.name = this.name;
if(this.episodeId) config.episodeId = this.episodeId;
if(this.posterLink) config.posterLink = this.posterLink;
if(this.links) config.links = this.links;
if(this.localLink) config.localLink = this.localLink;
return config;
}
}
exports.JsonObject = JsonObject;
exports.DownloadEpisode = DownloadEpisode;
exports.VideoPlayer = VideoPlayer;
exports.YoutubePlayer = YoutubePlayer;
exports.Anime = Anime;
exports.Episode = Episode;