commit
8ccc1db550
@ -0,0 +1,2 @@
|
|||||||
|
DATABASE_URL=mongodb://mongoadmin:something@192.168.71.137:27017
|
||||||
|
PORT=5010
|
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
Saved
|
@ -0,0 +1,260 @@
|
|||||||
|
const {parse} = require('node-html-parser');
|
||||||
|
const {decodeHTML} = require('../lib')
|
||||||
|
module.exports = class AsuraModule
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @property {Scanlator} scanlator - Scanlator name
|
||||||
|
* @typedef {String} Scanlator
|
||||||
|
* @pattern /^[\w-]+-scans$/
|
||||||
|
*/
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
this.scanlator = 'Asura-scans'
|
||||||
|
this.BaseLink = 'https://asuratoon.com/'
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} query
|
||||||
|
* @returns {Array<Manga>}
|
||||||
|
* @typedef {Object} Manga
|
||||||
|
* @property {Link} link - Manga Link
|
||||||
|
* @property {String} title - Manga Title
|
||||||
|
* @property {Link} img - Image Link
|
||||||
|
* @property {Number} latestChap - Latest Chapter Number
|
||||||
|
* @typedef {String} Link
|
||||||
|
*/
|
||||||
|
async Search(query)
|
||||||
|
{
|
||||||
|
return await fetch(this.BaseLink+'?s='+query)
|
||||||
|
.then(handleResponse)
|
||||||
|
.then(handleData)
|
||||||
|
.catch(handleError);
|
||||||
|
function handleResponse(response)
|
||||||
|
{
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
function handleError(error)
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
function handleData(data)
|
||||||
|
{
|
||||||
|
data = data.split('<div class="listupd">')[1].split('<div class="pagination">')[0]
|
||||||
|
const document = parse(data);
|
||||||
|
var payload = [];
|
||||||
|
for(var result of document.querySelectorAll('.bs > .bsx > a'))
|
||||||
|
{
|
||||||
|
var aux = {
|
||||||
|
link:result.rawAttrs.split('"')[1],
|
||||||
|
title:result.rawAttrs.split('"')[3],
|
||||||
|
img:'https://'+result.querySelectorAll('img')[0].rawAttrs.split('"')[1].split('https://')[2],
|
||||||
|
status: result.querySelector('.status')?.rawText || 'Ongoing',
|
||||||
|
latestChap:parseInt(result.querySelector('.epxs').innerText.split(' ')[1]),
|
||||||
|
};
|
||||||
|
payload.push(aux);
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} query
|
||||||
|
* @returns {Array<Manga>}
|
||||||
|
* @typedef {Object} Manga
|
||||||
|
* @property {Link} link - Manga Link
|
||||||
|
* @property {String} title - Manga Title
|
||||||
|
* @property {Link} img - Image Link
|
||||||
|
* @property {Number} latestChap - Latest Chapter Number
|
||||||
|
* @typedef {String} Link
|
||||||
|
*/
|
||||||
|
async SearchByTag(query, ammount=5)
|
||||||
|
{
|
||||||
|
return await fetch(this.BaseLink+'/genres/'+query)
|
||||||
|
.then(handleResponse)
|
||||||
|
.then(handleData)
|
||||||
|
.catch(handleError);
|
||||||
|
function handleResponse(response)
|
||||||
|
{
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
function handleError(error)
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
function handleData(data)
|
||||||
|
{
|
||||||
|
data = data.split('<div class="listupd">')[1].split('<div class="pagination">')[0]
|
||||||
|
const document = parse(data);
|
||||||
|
var payload = [];
|
||||||
|
var results = document.querySelectorAll('a')
|
||||||
|
var limit = results.length<ammount?results.length:ammount;
|
||||||
|
for(var i =0;i<limit; i++)
|
||||||
|
{
|
||||||
|
const result = results[i];
|
||||||
|
var aux = {
|
||||||
|
link:result.rawAttrs.split('"')[1],
|
||||||
|
title:decodeHTML(result.rawAttrs.split('"')[3]),
|
||||||
|
img:result.querySelectorAll('.limit > img')[0].rawAttrs.split('src="')[1].split(')/')[1].split('"')[0],
|
||||||
|
status:result.querySelector('.status')?.rawText||'Ongoing',
|
||||||
|
latestChap:parseInt(result.querySelectorAll('.epxs')[0].innerText.split(' ')[1]),
|
||||||
|
};
|
||||||
|
payload.push(aux);
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} name Manga/Manwha Name
|
||||||
|
* @param {Link} link Manga url (
|
||||||
|
* @returns {Array<Chapter>} - Chapters Array
|
||||||
|
* @typedef {Object} Chapter
|
||||||
|
* @property {Number} num - Chapter Number
|
||||||
|
* @property {String} link - Chapter Link
|
||||||
|
* @property {String} date - Chapter Release Date
|
||||||
|
* @typedef {String} Link
|
||||||
|
* @pattern ^(https?:\/\/)?([\w\d.-]+)\.([a-z]{2,})(:[0-9]+)?(\/\S*)?$
|
||||||
|
*/
|
||||||
|
async ChapterList(link, name)
|
||||||
|
{
|
||||||
|
return await fetch(link)
|
||||||
|
.then(handleResponse)
|
||||||
|
.then(handleData)
|
||||||
|
.catch(handleError);
|
||||||
|
function handleResponse(response)
|
||||||
|
{
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
function handleError(error)
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
function handleData(data)
|
||||||
|
{
|
||||||
|
|
||||||
|
const document = parse(data);
|
||||||
|
const chapterlist = document.querySelectorAll('ul')[1];
|
||||||
|
const chapters = chapterlist.querySelectorAll('li');
|
||||||
|
var payload = [];
|
||||||
|
try {
|
||||||
|
for(var chapter of chapters)
|
||||||
|
{
|
||||||
|
var aux = {
|
||||||
|
num:parseInt(chapter.rawAttrs.split('"')[1]),
|
||||||
|
link:chapter.querySelector('.eph-num').innerHTML.split('"')[1],
|
||||||
|
date:chapter.querySelector('.chapterdate').innerText
|
||||||
|
|
||||||
|
};
|
||||||
|
payload.push(aux);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('This manga should be called by url', error);
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Link} Link Chapter Link
|
||||||
|
* @returns {ChapterList}
|
||||||
|
* @typedef {Obejct} ChapterList
|
||||||
|
* @property {Array<Image>} List
|
||||||
|
* @property {Link} mangaLink - Manga Link
|
||||||
|
* @typedef {String} Image - Image link
|
||||||
|
* @typedef {String} Link
|
||||||
|
* @pattern ^(https?:\/\/)?([\w\d.-]+)\.([a-z]{2,})(:[0-9]+)?(\/\S*)?$
|
||||||
|
|
||||||
|
*/
|
||||||
|
async Chapter(Link)
|
||||||
|
{
|
||||||
|
return await fetch(Link)
|
||||||
|
.then(handleResponse)
|
||||||
|
.then(handleData)
|
||||||
|
.catch(handleError);
|
||||||
|
function handleResponse(response)
|
||||||
|
{
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
function handleError(error)
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
function handleData(data)
|
||||||
|
{
|
||||||
|
const document = parse(data);
|
||||||
|
let List = [];
|
||||||
|
let mangaLink = document.querySelectorAll('.allc')[0].querySelector('a')._rawAttrs.href;
|
||||||
|
for(var image of document.getElementById('readerarea').querySelectorAll('img'))
|
||||||
|
{
|
||||||
|
if(image.rawAttrs)
|
||||||
|
{
|
||||||
|
let index = image.rawAttrs.split('"').findIndex(el => el.includes('http'));
|
||||||
|
List.push(image.rawAttrs.split('"')[index].replace(/\n/g, ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {List, mangaLink}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} Title
|
||||||
|
* @returns {Manga}
|
||||||
|
* @typedef {Object} Manga
|
||||||
|
* @property {Link} Link - Manga Link
|
||||||
|
* @property {Link} img - Image Link
|
||||||
|
* @property {String} description - Latest Chapter Number
|
||||||
|
* @property {String} Type - Manga Type
|
||||||
|
* @property {String} Released - Year of Release
|
||||||
|
* @property {String} Author - Author's name
|
||||||
|
* @property {String} Artist - Artist Team
|
||||||
|
* @property {String} Serialization - Serialization Company
|
||||||
|
* @property {Number} latestChap - Latest Chap number
|
||||||
|
* @property {Array<String>} tags - Manga Tags
|
||||||
|
* @typedef {String} Link
|
||||||
|
*/
|
||||||
|
async GetManga(Link, title)
|
||||||
|
{
|
||||||
|
const auxTitle = title;
|
||||||
|
return await fetch(Link)
|
||||||
|
.then(handleResponse)
|
||||||
|
.then(handleData)
|
||||||
|
.catch(handleError);
|
||||||
|
function handleResponse(response)
|
||||||
|
{
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
function handleError(error)
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
function handleData(data)
|
||||||
|
{
|
||||||
|
|
||||||
|
const parsed = parse(data)
|
||||||
|
let description = [];
|
||||||
|
parsed.querySelectorAll('.entry-content-single')[0].querySelectorAll('p').forEach(p=>description.push(p.innerText));
|
||||||
|
description = decodeHTML(description.join(' '));
|
||||||
|
const img = 'https://'+parsed.querySelectorAll('img')[1].rawAttrs.split('src="')[1].split('https://')[2].split('"')[0];
|
||||||
|
let tags = parsed.querySelectorAll('.mgen')[0].structuredText.split('\n')[0].split(' ');
|
||||||
|
let latestChap = parseInt(parsed.querySelectorAll('.epcurlast')[0].rawText.split(' ')[1])
|
||||||
|
let table = parsed.querySelectorAll('.infox')[0].querySelectorAll('.fmed');
|
||||||
|
table.splice(-3);
|
||||||
|
const aux = {};
|
||||||
|
for (let i = 0; i < table.length; i++) {
|
||||||
|
const key = decodeHTML(table[i].querySelector('b').innerText);
|
||||||
|
aux[key] = decodeHTML(table[i].querySelector('span').innerText);
|
||||||
|
}
|
||||||
|
aux['Status'] = parsed.querySelectorAll('.imptdt')[0].innerText.split('\n')[1].split(' ')[1];
|
||||||
|
aux['Type'] = parsed.querySelectorAll('.imptdt')[1].innerText.split('\n')[1].split(' ')[1];
|
||||||
|
return {description, img, ...aux, tags, title:auxTitle, latestChap}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async hasTitle(title)
|
||||||
|
{
|
||||||
|
const Search = await this.Search(title);
|
||||||
|
if(!Array.isArray(Search)) return false
|
||||||
|
return Search.filter(Manga => {return Manga.title === title})[0];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,267 @@
|
|||||||
|
const {parse} = require('node-html-parser');
|
||||||
|
const {decodeHTML} = require('../lib')
|
||||||
|
module.exports = class ReaperModule
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property {Scanlator} scanlator - Scanlator name
|
||||||
|
* @typedef {String} Scanlator
|
||||||
|
* @pattern /^[\w-]+-scans$/
|
||||||
|
*/
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
this.scanlator = 'Reaper-scans';
|
||||||
|
this.BaseLink = 'https://reaper-scans.com/';
|
||||||
|
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} query
|
||||||
|
* @returns {Array<Manga>}
|
||||||
|
* @typedef {Object} Manga
|
||||||
|
* @property {Link} link - Manga Link
|
||||||
|
* @property {String} title - Manga Title
|
||||||
|
* @property {String} Status - Manga Status
|
||||||
|
* @property {Link} img - Image Link
|
||||||
|
* @property {Number} latestChap - Latest Chapter Number
|
||||||
|
* @typedef {String} Link
|
||||||
|
*/
|
||||||
|
async Search(query)
|
||||||
|
{
|
||||||
|
return await fetch(this.BaseLink+'?s='+query)
|
||||||
|
.then(handleResponse)
|
||||||
|
.then(handleData)
|
||||||
|
.catch(handleError);
|
||||||
|
function handleResponse(response)
|
||||||
|
{
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
function handleError(error)
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
function handleData(data)
|
||||||
|
{
|
||||||
|
data = data.split('<div class="listupd">')[1].split('<div class="pagination">')[0]
|
||||||
|
const document = parse(data);
|
||||||
|
let payload = [];
|
||||||
|
for(var result of document.querySelectorAll('a'))
|
||||||
|
{
|
||||||
|
let aux = {
|
||||||
|
link:result.rawAttrs.split('"')[1],
|
||||||
|
title:result.rawAttrs.split('"')[3],
|
||||||
|
img:result.querySelectorAll('.limit > img')[0].rawAttrs.split('"')[1],
|
||||||
|
status:result.querySelector('.status')?.rawText||'Ongoing',
|
||||||
|
latestChap:parseInt(result.childNodes[1].childNodes[1].childNodes[0].rawText.split(" ")[1]),
|
||||||
|
};
|
||||||
|
payload.push(aux);
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} query
|
||||||
|
* @returns {Array<Manga>}
|
||||||
|
* @typedef {Object} Manga
|
||||||
|
* @property {Link} link - Manga Link
|
||||||
|
* @property {String} title - Manga Title
|
||||||
|
* @property {Link} img - Image Link
|
||||||
|
* @property {Number} latestChap - Latest Chapter Number
|
||||||
|
* @typedef {String} Link
|
||||||
|
*/
|
||||||
|
async SearchByTag(query, ammount=5)
|
||||||
|
{
|
||||||
|
return await fetch(this.BaseLink+'genres/'+query)
|
||||||
|
.then(handleResponse)
|
||||||
|
.then(handleData)
|
||||||
|
.catch(handleError);
|
||||||
|
function handleResponse(response)
|
||||||
|
{
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
function handleError(error)
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
function handleData(data)
|
||||||
|
{
|
||||||
|
data = data.split('<div class="listupd">')[1].split('<div class="pagination">')[0]
|
||||||
|
const document = parse(data);
|
||||||
|
var payload = [];
|
||||||
|
var results = document.querySelectorAll('a')
|
||||||
|
var limit = results.length<ammount?results.length:ammount;
|
||||||
|
for(var i =0;i<limit; i++)
|
||||||
|
{
|
||||||
|
const result = results[i];
|
||||||
|
var aux = {
|
||||||
|
link:result.rawAttrs.split('"')[1],
|
||||||
|
title:decodeHTML(result.rawAttrs.split('"')[3]),
|
||||||
|
img:result.querySelectorAll('.limit > img')[0].rawAttrs.split('data-src="')[1].split('"')[0],
|
||||||
|
status:result.querySelector('.status')?.rawText||'Ongoing',
|
||||||
|
latestChap:parseInt(result.childNodes[1].childNodes[1].childNodes[0].rawText.split(" ")[1]),
|
||||||
|
};
|
||||||
|
payload.push(aux);
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} name Manga/Manwha Name
|
||||||
|
* @param {Link} link Manga url (
|
||||||
|
* @returns {Array<Chapter>} - Chapters Array
|
||||||
|
* @typedef {Object} Chapter
|
||||||
|
* @property {Number} num - Chapter Number
|
||||||
|
* @property {String} link - Chapter Link
|
||||||
|
* @property {String} date - Chapter Release Date
|
||||||
|
* @typedef {String} Link
|
||||||
|
* @pattern ^(https?:\/\/)?([\w\d.-]+)\.([a-z]{2,})(:[0-9]+)?(\/\S*)?$
|
||||||
|
*/
|
||||||
|
async ChapterList(link, name)
|
||||||
|
{
|
||||||
|
return await fetch(link)
|
||||||
|
.then(handleResponse)
|
||||||
|
.then(handleData)
|
||||||
|
.catch(handleError);
|
||||||
|
function handleResponse(response)
|
||||||
|
{
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
function handleError(error)
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
function handleData(data)
|
||||||
|
{
|
||||||
|
|
||||||
|
const document = parse(data);
|
||||||
|
const chapterlist = document.querySelectorAll('ul')[2];
|
||||||
|
const chapters = parse(chapterlist).querySelectorAll('li');
|
||||||
|
var payload = [];
|
||||||
|
try {
|
||||||
|
for(var chapter of chapters)
|
||||||
|
{
|
||||||
|
var aux = {
|
||||||
|
num:parseInt(chapter.rawAttrs.split('"')[1]),
|
||||||
|
link:chapter.childNodes[0].childNodes[0].childNodes[1].rawAttrs.split('"')[1],
|
||||||
|
date:chapter.childNodes[0].childNodes[0].childNodes[1].childNodes[3].rawText,
|
||||||
|
|
||||||
|
};
|
||||||
|
payload.push(aux);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('This manga should be called by url', error);
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Link} Link Chapter Link
|
||||||
|
* @returns {ChapterList}
|
||||||
|
* @typedef {Obejct} ChapterList
|
||||||
|
* @property {Array<Image>} List
|
||||||
|
* @property {Link} mangaLink - Manga Link
|
||||||
|
* @typedef {String} Image - Image link
|
||||||
|
* @typedef {String} Link
|
||||||
|
* @pattern ^(https?:\/\/)?([\w\d.-]+)\.([a-z]{2,})(:[0-9]+)?(\/\S*)?$
|
||||||
|
|
||||||
|
*/
|
||||||
|
async Chapter(Link)
|
||||||
|
{
|
||||||
|
return await fetch(Link)
|
||||||
|
.then(handleResponse)
|
||||||
|
.then(handleData)
|
||||||
|
.catch(handleError);
|
||||||
|
function handleResponse(response)
|
||||||
|
{
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
function handleError(error)
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
function handleData(data)
|
||||||
|
{
|
||||||
|
const document = parse(data);
|
||||||
|
var List = [];
|
||||||
|
let mangaLink = document.querySelectorAll('.allc')[0].querySelector('a')._rawAttrs.href;
|
||||||
|
for(var image of document.querySelectorAll('p')[1].childNodes)
|
||||||
|
{
|
||||||
|
if(image.rawAttrs)
|
||||||
|
{
|
||||||
|
let index = image.rawAttrs.split('"').findIndex(el => el.includes('http'));
|
||||||
|
List.push(image.rawAttrs.split('"')[index].replace(/\n/g, ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {List, mangaLink}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} Title
|
||||||
|
* @returns {Manga}
|
||||||
|
* @typedef {Object} Manga
|
||||||
|
* @property {Link} Link - Manga Link
|
||||||
|
* @property {Link} img - Image Link
|
||||||
|
* @property {String} description - Latest Chapter Number
|
||||||
|
* @property {String} Type - Manga Type
|
||||||
|
* @property {String} Released - Year of Release
|
||||||
|
* @property {String} Author - Author's name
|
||||||
|
* @property {String} Artist - Artist Team
|
||||||
|
* @property {String} Serialization - Serialization Company
|
||||||
|
* @property {Number} latestChap - Latest Chap number
|
||||||
|
* @property {Array<String>} tags - Manga Tags
|
||||||
|
* @typedef {String} Link
|
||||||
|
*/
|
||||||
|
async GetManga(Link, title)
|
||||||
|
{
|
||||||
|
const auxTitle = title;
|
||||||
|
return await fetch(Link)
|
||||||
|
.then(handleResponse)
|
||||||
|
.then(handleData)
|
||||||
|
.catch(handleError);
|
||||||
|
function handleResponse(response)
|
||||||
|
{
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
function handleError(error)
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
function handleData(data)
|
||||||
|
{
|
||||||
|
|
||||||
|
const parsed = parse(data)
|
||||||
|
let description = parsed.querySelectorAll('.entry-content-single')[0].innerText
|
||||||
|
description = decodeHTML(description);
|
||||||
|
if(description.split('/>').length>1) description = description.split('/>')[1]
|
||||||
|
const img = parsed.querySelectorAll('img')[1].rawAttrs.split('src="')[1].split('"')[0].includes('data:')? parsed.querySelectorAll('img')[1].rawAttrs.split('data-src="')[1].split('"')[0]:parsed.querySelectorAll('img')[1].rawAttrs.split('src="')[1].split('"')[0];
|
||||||
|
let table = parsed.querySelectorAll('.infotable')[0].childNodes[0].structuredText.split('\n');
|
||||||
|
let tags = parsed.querySelectorAll('.seriestugenre')[0].structuredText.split('\n')[0].split(' ');
|
||||||
|
let latestChap = parseInt(parsed.querySelectorAll('.epcurlast')[0].rawText.split(' ')[1])
|
||||||
|
table.splice(-6);
|
||||||
|
const aux = {};
|
||||||
|
for (let i = 0; i < table.length; i += 2) {
|
||||||
|
const key = table[i].trim();
|
||||||
|
aux[key] = table[i + 1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {description, img, ...aux, tags, title:auxTitle, latestChap}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} title Manga Title
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
async hasTitle(title)
|
||||||
|
{
|
||||||
|
const Search = await this.Search(title);
|
||||||
|
if(!Array.isArray(Search)) return false
|
||||||
|
return Search.filter(Manga => {return Manga.title === title})[0];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "a123456789",
|
||||||
|
"password2": "a123456789",
|
||||||
|
"saveFolder": "./public/Saved",
|
||||||
|
"hash": "e941d5c52a2f5aca8237ef40a46749d8619c213683d080f753c5d86f314ae270"
|
||||||
|
}
|
@ -0,0 +1,405 @@
|
|||||||
|
const {Search, Chapter, Manga,SearchByTag, isValidID, Modules, Baker, Crypto, cookieParser} = require('../lib.js')
|
||||||
|
const mongoose = require('mongoose')
|
||||||
|
const FavoriteModel = require('../models/favorite.js');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { isNullOrUndefined } = require('util');
|
||||||
|
|
||||||
|
exports.home = async (req,res)=>
|
||||||
|
{
|
||||||
|
const cookieStr = req.headers.cookie;
|
||||||
|
if(!cookieStr) return res.render('home.ejs',{data:null});
|
||||||
|
const cookie = cookieParser(cookieStr, 'hash');
|
||||||
|
res.render('home.ejs', {data:cookie});
|
||||||
|
}
|
||||||
|
exports.searchBar = (req, res)=>
|
||||||
|
{
|
||||||
|
res.render('searchBar.ejs');
|
||||||
|
}
|
||||||
|
exports.search = async (req, res)=>
|
||||||
|
{
|
||||||
|
if(!req.body?.searchstring) return res.sendStatus(404)
|
||||||
|
var data = await new Search(req.body.searchstring).search();
|
||||||
|
if(!Array.isArray(data)) return res.sendStatus(404);
|
||||||
|
const favorites = await FavoriteModel.find()
|
||||||
|
const favoriteScanlators = favorites.map(favorite => favorite.scanlator);
|
||||||
|
data.forEach((item, index) =>
|
||||||
|
{
|
||||||
|
if (favoriteScanlators.includes(item.scanlator))
|
||||||
|
{
|
||||||
|
item.Results.find(result =>
|
||||||
|
{
|
||||||
|
const matchingFavorite = favorites.find(favorite =>
|
||||||
|
{
|
||||||
|
return result.title === favorite.title && item.scanlator === favorite.scanlator;
|
||||||
|
});
|
||||||
|
if (matchingFavorite)
|
||||||
|
{
|
||||||
|
result.favorite = matchingFavorite._id;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
data = data.filter(item => item.Results.length > 0)
|
||||||
|
data.sort((a,b)=>
|
||||||
|
{
|
||||||
|
const scanlatorA = a.scanlator.toLowerCase();
|
||||||
|
const scanlatorB = b.scanlator.toLowerCase();
|
||||||
|
return scanlatorA.localeCompare(scanlatorB);
|
||||||
|
})
|
||||||
|
res.render('searchResults.ejs', {data})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
exports.bookmark = (worker)=>
|
||||||
|
{
|
||||||
|
return async (req, res)=>
|
||||||
|
{
|
||||||
|
if (!req.params || typeof req.params !== 'object' || !('scanlator' in req.params) || !('title' in req.params)) return res.sendStatus(404);
|
||||||
|
const {scanlator, title, idorLink} = req.params;
|
||||||
|
const {cookie} = req.headers;
|
||||||
|
if(!cookie) return res.sendStatus(404);
|
||||||
|
const config = require('../config.json');
|
||||||
|
if(cookieParser(cookie, 'hash')!=config.hash) return res.sendStatus(404);
|
||||||
|
if(isValidID(idorLink))
|
||||||
|
{
|
||||||
|
worker.send({ action:'remove', idorLink})
|
||||||
|
return await FavoriteModel.findOneAndDelete({_id:idorLink}).then((deleted)=>
|
||||||
|
{
|
||||||
|
return res.render('bookmark.ejs', {scanlator, title, link:encodeURIComponent(deleted.link)})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const scanlatorExists = await new Modules(scanlator).exists();
|
||||||
|
if(!scanlatorExists) return res.sendStatus(404);
|
||||||
|
const scanlatorHasTitle = await new Modules(scanlator).titleExists(title);
|
||||||
|
if(!scanlatorHasTitle) return res.sendStatus(404);
|
||||||
|
|
||||||
|
const fav = new FavoriteModel()
|
||||||
|
fav.scanlator = scanlator;
|
||||||
|
fav.title = title;
|
||||||
|
fav.link = idorLink;
|
||||||
|
await fav.save();
|
||||||
|
|
||||||
|
worker.send({action:'add', id:fav._id})
|
||||||
|
res.render('bookmarked.ejs', {scanlator, title, link:idorLink, id:fav._id});
|
||||||
|
let manga;
|
||||||
|
try {
|
||||||
|
manga = await new Manga(scanlator, idorLink, title).get();
|
||||||
|
} catch (error) {
|
||||||
|
return FavoriteModel.findByIdAndDelete(fav._id)
|
||||||
|
}
|
||||||
|
const {tags} = manga;
|
||||||
|
FavoriteModel.findByIdAndUpdate(fav._id,{tags}, { new: true})
|
||||||
|
.then((doc, err) =>
|
||||||
|
{
|
||||||
|
if (err) console.error('test',err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.manga = async (req, res)=>
|
||||||
|
{
|
||||||
|
const {scanlator, link, title} = req.params;
|
||||||
|
const scanlatorExists = await new Modules(scanlator).exists();
|
||||||
|
if(!scanlatorExists) return res.sendStatus(404);
|
||||||
|
const scanlatorHasTitle = await new Modules(scanlator).titleExists(title);
|
||||||
|
if(!scanlatorHasTitle) return res.sendStatus(404);
|
||||||
|
let manga;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
manga = await new Manga(scanlator,link, title).get()
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
res.sendStatus(404);
|
||||||
|
}
|
||||||
|
res.render('mangaPage.ejs', {data:{...manga, scanlator}});
|
||||||
|
|
||||||
|
}
|
||||||
|
exports.chapter = async (req, res)=>
|
||||||
|
{
|
||||||
|
const {scanlator, title, chapter, link} = req.params;
|
||||||
|
//IF its already there even with wrong things its there just show it
|
||||||
|
const fileExists = fs.existsSync(`./public/Saved/${scanlator}/${title}/CH_${chapter}`);
|
||||||
|
if(fileExists)
|
||||||
|
{
|
||||||
|
const imgs =fs.readdirSync(`./public/Saved/${scanlator}/${title}/CH_${chapter}`);
|
||||||
|
const latestChap = fs.readdirSync(`./public/Saved/${scanlator}/${title}`).length
|
||||||
|
const List = imgs.map(filename => {return `./Saved/${scanlator}/${title}/CH_${chapter}/${filename}`});
|
||||||
|
const mangaLink = await new Chapter(scanlator, link, title, chapter).getMangaLink();
|
||||||
|
return res.render('display.ejs', {data:{title,latestChap,scanlator, chapterNum:parseInt(chapter), mangaLink, List}});
|
||||||
|
}
|
||||||
|
//If it isn't make sure there is no bad params being passed
|
||||||
|
const scanlatorExists = await new Modules(scanlator).exists();
|
||||||
|
if(!scanlatorExists) return res.sendStatus(404);
|
||||||
|
const scanlatorHasTitle = await new Modules(scanlator).titleExists(title);
|
||||||
|
if(!scanlatorHasTitle) return res.sendStatus(404);
|
||||||
|
let manga;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
manga = await new Chapter(scanlator, link, title,chapter).get();
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
|
res.render('display.ejs', {data:{...manga, scanlator, chapterNum:parseInt(chapter)}});
|
||||||
|
|
||||||
|
}
|
||||||
|
exports.favorites = async (req, res)=>
|
||||||
|
{
|
||||||
|
const favs = await FavoriteModel.find();
|
||||||
|
var mangas = [];
|
||||||
|
if(favs.length == 0) return res.render('favorites.ejs', {isEmpty:true, mangas})
|
||||||
|
await Promise.all(Object.keys(favs).map(async (key) =>
|
||||||
|
{
|
||||||
|
const manga = favs[key];
|
||||||
|
var aux = await new Manga(manga.scanlator, manga.link, manga.title).get();
|
||||||
|
if(aux)
|
||||||
|
{
|
||||||
|
aux.favorite = manga._id;
|
||||||
|
mangas.push(aux);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
function getUniqueScanlators(items)
|
||||||
|
{
|
||||||
|
const scanlators = {};
|
||||||
|
items.forEach(item => {
|
||||||
|
scanlators[item.scanlator] = true;
|
||||||
|
});
|
||||||
|
return Object.keys(scanlators);
|
||||||
|
}
|
||||||
|
const uniqueScanlators = getUniqueScanlators(mangas);
|
||||||
|
const scanlatorsArray = uniqueScanlators.map(scanlator =>
|
||||||
|
{
|
||||||
|
var scanlatorItems = mangas.filter(manga => manga.scanlator === scanlator);
|
||||||
|
if(!scanlatorItems) return res.sendStatus(500);
|
||||||
|
scanlatorItems.sort((a, b) =>
|
||||||
|
{
|
||||||
|
if (a.title.toUpperCase() < b.title.toUpperCase()) return -1;
|
||||||
|
else return 1;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
scanlator,
|
||||||
|
Results: scanlatorItems
|
||||||
|
};
|
||||||
|
});
|
||||||
|
scanlatorsArray.sort((a,b)=>
|
||||||
|
{
|
||||||
|
const scanlatorA = a.scanlator.toLowerCase();
|
||||||
|
const scanlatorB = b.scanlator.toLowerCase();
|
||||||
|
return scanlatorA.localeCompare(scanlatorB);
|
||||||
|
})
|
||||||
|
res.render('favorites.ejs', {isEmpty:false, mangas:scanlatorsArray})
|
||||||
|
}
|
||||||
|
exports.recommended = async (req,res)=>
|
||||||
|
{
|
||||||
|
const favorites = await FavoriteModel.find();
|
||||||
|
const favoriteScanlators = favorites.map(favorite => favorite.scanlator);
|
||||||
|
let favTags = [];
|
||||||
|
for(var i = 0; i<favorites.length; i++)
|
||||||
|
{
|
||||||
|
favTags.push(...favorites[i].tags)
|
||||||
|
}
|
||||||
|
const counted = favTags.reduce((acc, curr)=>
|
||||||
|
{
|
||||||
|
acc[curr] = (acc[curr]||0)+1;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
const mostCommonTags = [...new Set(favTags)].sort((a,b)=>
|
||||||
|
{
|
||||||
|
if(counted[b] !== counted[a])
|
||||||
|
{
|
||||||
|
return counted[b]-counted[a];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return favTags.indexOf(a)-favTags.indexOf(b);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const firstTwoTags =favoriteScanlators.length <=0 ? ['action', 'shounen'] : mostCommonTags.slice(0,2);
|
||||||
|
let recommended = [];
|
||||||
|
for(var i =0; i<2; i++)
|
||||||
|
{
|
||||||
|
const results = await new SearchByTag(firstTwoTags[i]).search();
|
||||||
|
recommended.push(results);
|
||||||
|
}
|
||||||
|
const groupedByScanlator = recommended.reduce((acc, currentArray) =>
|
||||||
|
{
|
||||||
|
currentArray.forEach(({ Results, scanlator }) =>
|
||||||
|
{
|
||||||
|
if (!acc[scanlator])
|
||||||
|
{
|
||||||
|
acc[scanlator] = { Results: [], scanlator };
|
||||||
|
}
|
||||||
|
const uniqueResults = Results.filter(result =>
|
||||||
|
!acc[scanlator].Results.some(existingResult => existingResult.title === result.title)
|
||||||
|
);
|
||||||
|
acc[scanlator].Results = acc[scanlator].Results.concat(uniqueResults);
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
recommended = Object.values(groupedByScanlator);
|
||||||
|
|
||||||
|
recommended.forEach((item, index) =>
|
||||||
|
{
|
||||||
|
if (favoriteScanlators.includes(item.scanlator))
|
||||||
|
{
|
||||||
|
item.Results.find(result =>
|
||||||
|
{
|
||||||
|
const matchingFavorite = favorites.find(favorite =>
|
||||||
|
{
|
||||||
|
return result.title === favorite.title && item.scanlator === favorite.scanlator;
|
||||||
|
});
|
||||||
|
if (matchingFavorite)
|
||||||
|
{
|
||||||
|
result.favorite = matchingFavorite._id;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
recommended.sort((a,b)=>
|
||||||
|
{
|
||||||
|
const scanlatorA = a.scanlator.toLowerCase();
|
||||||
|
const scanlatorB = b.scanlator.toLowerCase();
|
||||||
|
return scanlatorA.localeCompare(scanlatorB);
|
||||||
|
})
|
||||||
|
res.render('searchResults.ejs', {data:recommended});
|
||||||
|
}
|
||||||
|
exports.chapterNavInfo = async (req, res)=>
|
||||||
|
{
|
||||||
|
const {scanlator, title, mangaLink} = req.params;
|
||||||
|
const scanlatorExists = await new Modules(scanlator).exists();
|
||||||
|
if(!scanlatorExists) return res.sendStatus(404);
|
||||||
|
const scanlatorHasTitle = await new Modules(scanlator).titleExists(title);
|
||||||
|
if(!scanlatorHasTitle) return res.sendStatus(404);
|
||||||
|
let chapter = parseInt(req.params.chapter);
|
||||||
|
let manga;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
manga = await new Manga(scanlator, mangaLink, title).get();
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
|
const {latestChap, List} = manga;
|
||||||
|
List.sort((a,b)=> {return a.num - b.num});
|
||||||
|
let nextChapterLink = chapter>= latestChap? '' : List[chapter].link;
|
||||||
|
let prevChapterLink = chapter<=0? '' : List[chapter-1].link;
|
||||||
|
res.render('chapterNav.ejs', {data:{title,latestChap,scanlator, chapterNum:chapter,mangaLink, nextChapterLink, prevChapterLink}})
|
||||||
|
}
|
||||||
|
exports.hasConfig = (req, res, next) =>
|
||||||
|
{
|
||||||
|
const exists = fs.existsSync('./config.json');
|
||||||
|
if(!exists) return this.config(req,res)
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
exports.config = async (req, res)=>
|
||||||
|
{
|
||||||
|
function getSubDirectories(dir)
|
||||||
|
{
|
||||||
|
let protectedSubDirs = ['node_modules', '.git', 'controllers', 'models', 'Modules', 'css','images', 'js', 'routes', 'views'];
|
||||||
|
const map = new Map();
|
||||||
|
const dirs = fs.readdirSync(dir)
|
||||||
|
.filter(file => fs.statSync(path.join(dir, file)).isDirectory())
|
||||||
|
.filter(file => !protectedSubDirs.includes(file))
|
||||||
|
.filter(file=> !file.includes('scans'));
|
||||||
|
for(let i =0; i<dirs.length; i++)
|
||||||
|
{
|
||||||
|
map.set(dirs[i], getSubDirectories(path.join(dir, dirs[i])));
|
||||||
|
}
|
||||||
|
return map.size==0?'':map;
|
||||||
|
}
|
||||||
|
function getPaths(map, prefix='.')
|
||||||
|
{
|
||||||
|
const paths = [];
|
||||||
|
for (const [key, value] of map.entries())
|
||||||
|
{
|
||||||
|
const fullPath = `${prefix}/${key}`;
|
||||||
|
if (value instanceof Map)
|
||||||
|
{
|
||||||
|
paths.push(...getPaths(value, fullPath));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
paths.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subDirs = await getSubDirectories('./');
|
||||||
|
const data = getPaths(subDirs)
|
||||||
|
|
||||||
|
res.render('config.ejs', {data});
|
||||||
|
}
|
||||||
|
exports.configPost = async (req, res)=>
|
||||||
|
{
|
||||||
|
const {username, password, password2, saveFolder} = req.body;
|
||||||
|
const {body} = req;
|
||||||
|
if(!username || !password || !password2) return res.sendStatus(404);
|
||||||
|
if(username.length < 4) return res.sendStatus(404);
|
||||||
|
if(password !== password2) return res.sendStatus(404);
|
||||||
|
if(!checkValidPath(saveFolder)) return res.sendStatus(404);
|
||||||
|
if(!saveFolder) body.saveFolder = './public/Saved'
|
||||||
|
body['hash'] = new Crypto(username, password).Hash;
|
||||||
|
await fs.writeFileSync('./config.json', JSON.stringify(body));
|
||||||
|
res.redirect('/');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {String} dir
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
function checkValidPath(dir)
|
||||||
|
{
|
||||||
|
if(!dir) return true; //by pass when there is no savePath (use default);
|
||||||
|
dir = dir;
|
||||||
|
return !path.isAbsolute(dir)
|
||||||
|
}
|
||||||
|
exports.loginPage = async (req, res)=>
|
||||||
|
{
|
||||||
|
res.render('login.ejs');
|
||||||
|
}
|
||||||
|
exports.login = async (req, res)=>
|
||||||
|
{
|
||||||
|
const {username, password} = req.body;
|
||||||
|
if(!username || !password ) return res.sendStatus(404);
|
||||||
|
const config = require('../config.json');
|
||||||
|
if(username != config.username || password != config.password) return res.sendStatus(404);
|
||||||
|
await Baker(res, 'hash', config.hash, 24)
|
||||||
|
//TODO: change this to dashboard
|
||||||
|
res.redirect('/');
|
||||||
|
}
|
||||||
|
exports.logout = async (req, res)=>
|
||||||
|
{
|
||||||
|
const config = require('../config.json');
|
||||||
|
const cookieStr = req.headers.cookie;
|
||||||
|
if(!cookieStr) return res.redirect('/');
|
||||||
|
const hash = cookieParser(cookieStr, 'hash');
|
||||||
|
if(hash != config.hash) return res.redirect('/');
|
||||||
|
await Baker(res, 'hash', '')
|
||||||
|
res.render('dashboard.ejs');
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.errorPage = (req, res)=>
|
||||||
|
{
|
||||||
|
res.render('error.ejs')
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.chapterRead = async (req, res)=>
|
||||||
|
{
|
||||||
|
console.log('ChapterRead')
|
||||||
|
res.sendStatus(200);
|
||||||
|
//Send to db;
|
||||||
|
}
|
||||||
|
exports.dashboard = async (req, res)=>
|
||||||
|
{
|
||||||
|
let data = 0;
|
||||||
|
res.render('dashboard.ejs', {data});
|
||||||
|
}
|
@ -0,0 +1,286 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const mongoURI = process.env.DATABASE_URL;
|
||||||
|
mongoose.connect(mongoURI);
|
||||||
|
mongoose.connection.on('connected', ()=>{console.log('MongoDB - Worker - Connected')})
|
||||||
|
.on('disconnected', ()=>{console.log('MongoDB - Disconnect')})
|
||||||
|
.on('error', (error)=>console.log('Mongoose Error:', error));
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { Stream } = require('stream');
|
||||||
|
const {Manga, Chapter} = require('./lib');
|
||||||
|
const FavModel = require('./models/favorite')
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const Emitter = new EventEmitter();
|
||||||
|
let config;
|
||||||
|
if(fs.existsSync('./config.json'))
|
||||||
|
{
|
||||||
|
config = require('./config.json')
|
||||||
|
}
|
||||||
|
const BaseDir = config?.saveFolder || './public/Saved';
|
||||||
|
|
||||||
|
var Queue = [];
|
||||||
|
var slots = 2;
|
||||||
|
|
||||||
|
var shouldKeepOnDownloading = true;
|
||||||
|
|
||||||
|
// updateQueue();
|
||||||
|
// var queueUpdateTimer = setInterval(() => {
|
||||||
|
// updateQueue();
|
||||||
|
// }, 5*60*1000);
|
||||||
|
|
||||||
|
|
||||||
|
// process.on('message', m=>
|
||||||
|
// {
|
||||||
|
// if(m.action==='add')
|
||||||
|
// {
|
||||||
|
// clearInterval(queueUpdateTimer);
|
||||||
|
// updateQueue();
|
||||||
|
// queueUpdateTimer = setInterval(() => {
|
||||||
|
// updateQueue();
|
||||||
|
// }, 5*60*1000);
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if(m.action==='remove') return removeFromQueue(m.id)
|
||||||
|
// if(m.action==='pause') return shouldKeepOnDownloading=false;
|
||||||
|
// })
|
||||||
|
|
||||||
|
function removeFromQueue(id)
|
||||||
|
{
|
||||||
|
const ItemDownloading = Queue.filter(manga => manga.inProgress == true)[0];
|
||||||
|
if(ItemDownloading) shouldKeepOnDownloading = false;
|
||||||
|
Queue = Queue.filter(manga => manga._id !== id);
|
||||||
|
|
||||||
|
}
|
||||||
|
Emitter.on('queueUpdate', ()=>
|
||||||
|
{
|
||||||
|
if(!Queue[0]) return
|
||||||
|
if(!Queue[0].inProgress)
|
||||||
|
{
|
||||||
|
Queue[0].inProgress = true;
|
||||||
|
download(Queue[0])
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
async function updateQueue()
|
||||||
|
{
|
||||||
|
shouldKeepOnDownloading = true;
|
||||||
|
const Scanlators = fs.readdirSync(BaseDir);
|
||||||
|
const Favs = await FavModel.find();
|
||||||
|
let Mangas = [];
|
||||||
|
for(let i =0; i<Scanlators.length; i++)
|
||||||
|
{
|
||||||
|
let mangas = fs.readdirSync(path.join(BaseDir, Scanlators[i]));
|
||||||
|
if(mangas.length < 0) continue
|
||||||
|
for(var j = 0; j<mangas.length; j++)
|
||||||
|
{
|
||||||
|
let chapters = fs.readdirSync(path.join(BaseDir, Scanlators[i], mangas[j]));
|
||||||
|
if(chapters.length==0) continue
|
||||||
|
chapters.sort((a, b) => { return parseInt(a.split('_')[1]) - parseInt(b.split('_')[1]) });
|
||||||
|
let latestDownloaded = parseInt(chapters[chapters.length-1].split('_')[1]);
|
||||||
|
let manga = {scanlator:Scanlators[i], title:mangas[j], latestDownloaded}
|
||||||
|
Mangas.push(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let FavedAndDownloaded = [];
|
||||||
|
let FavedButNotDownloaded = null;
|
||||||
|
for (let i = 0; i < Mangas.length; i++)
|
||||||
|
{
|
||||||
|
FavedAndDownloaded = FavedAndDownloaded.concat(Favs.filter(item =>
|
||||||
|
{
|
||||||
|
return (item.scanlator === Mangas[i].scanlator && item.title === Mangas[i].title);
|
||||||
|
}).map((matchedItem) =>{
|
||||||
|
return ({
|
||||||
|
...matchedItem._doc,
|
||||||
|
latestDownloaded: Mangas[i].latestDownloaded
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
FavedButNotDownloaded = Favs.filter(fav =>
|
||||||
|
{
|
||||||
|
// return !FavedAndDownloaded.includes(fav)
|
||||||
|
return !FavedAndDownloaded.some(fad => fad._id === fav._id);
|
||||||
|
});
|
||||||
|
if(FavedButNotDownloaded)
|
||||||
|
{
|
||||||
|
for(let i = 0; i < FavedButNotDownloaded.length; i++)
|
||||||
|
{
|
||||||
|
(async(i)=>
|
||||||
|
{
|
||||||
|
const {title, link, scanlator, _id} = FavedButNotDownloaded[i];
|
||||||
|
const manga = await new Manga(scanlator, link, title).get();
|
||||||
|
let id = _id.toString();
|
||||||
|
addToQueue(manga, id);
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(FavedAndDownloaded)
|
||||||
|
{
|
||||||
|
for(let i = 0; i<FavedAndDownloaded.length; i++)
|
||||||
|
{
|
||||||
|
(async(i)=>
|
||||||
|
{
|
||||||
|
const {title, link, scanlator, latestDownloaded, _id} = FavedAndDownloaded[i];
|
||||||
|
const manga = await new Manga(scanlator, link, title).get();
|
||||||
|
let id = _id.toString();
|
||||||
|
checkForMissingFiles(manga)
|
||||||
|
if(latestDownloaded<manga.latestChap) addToQueue(manga, id, latestDownloaded);
|
||||||
|
})(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function addToQueue(manga, _id,startFrom=0)
|
||||||
|
{
|
||||||
|
const List = manga.List.filter(item => item.num > startFrom);
|
||||||
|
Queue.push({...manga, _id, List, startFrom})
|
||||||
|
Emitter.emit('queueUpdate', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function checkForMissingFiles(manga)
|
||||||
|
{
|
||||||
|
const {scanlator, title, List} = manga;
|
||||||
|
let auxList = List.sort((a,b)=>{return a.num - b.num})
|
||||||
|
for(var i = 0; i<auxList.length; i++)
|
||||||
|
{
|
||||||
|
const {link} = auxList[i];
|
||||||
|
const destPath = path.join(BaseDir, scanlator, title, 'CH_'+(i+1));
|
||||||
|
const chapterDownloaded = fs.existsSync(destPath);
|
||||||
|
if(!chapterDownloaded)
|
||||||
|
{
|
||||||
|
if(shouldKeepOnDownloading)
|
||||||
|
{
|
||||||
|
const chapter = await new Chapter(scanlator, link, title, i+1).get();
|
||||||
|
await downloadChapter(chapter);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const chapterDir = fs.readdirSync(destPath);
|
||||||
|
const chapter = await new Chapter(scanlator, link, title, i+1).get();
|
||||||
|
let auxChapterDir = chapterDir.map(item=>{return item.split('.')[0]})
|
||||||
|
if(!chapter?.List) continue;
|
||||||
|
var auxChapterList = chapter.List.map(item=>{return item.split('/')[item.split('/').length-1].split('.')[0]})
|
||||||
|
var incompleteImages = [];
|
||||||
|
for(var k = 0; k<auxChapterDir.length; k++)
|
||||||
|
{
|
||||||
|
const auxDir = chapterDir.sort();
|
||||||
|
const _path = path.join(destPath, auxDir[k]);
|
||||||
|
const isComplete = await isImageComplete(_path);
|
||||||
|
if(!isComplete) incompleteImages.push(auxDir[k].split('.')[0])
|
||||||
|
}
|
||||||
|
const missingImages =[...incompleteImages, ...auxChapterList.filter(item=>!auxChapterDir.includes(item))];
|
||||||
|
if(missingImages.length == 0) continue;
|
||||||
|
for(var j = 0; j<missingImages.length; j++)
|
||||||
|
{
|
||||||
|
const imageLink = chapter.List.filter(item=>item.split('/')[item.split('/').length-1].split('.')[0] == missingImages[j])[0]
|
||||||
|
const destination = `${destPath}/${imageLink.split('/')[(imageLink.split('/').length - 1)]}`
|
||||||
|
if(shouldKeepOnDownloading)
|
||||||
|
{
|
||||||
|
const download = await downloadImage(imageLink,destination, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Path} filePath
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
async function isImageComplete(filePath)
|
||||||
|
{
|
||||||
|
async function IsFileComplete(filePath) {
|
||||||
|
try {
|
||||||
|
await sharp(filePath, { sequentialRead: true })
|
||||||
|
.raw()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function download(manga)
|
||||||
|
{
|
||||||
|
if(slots <=0) return
|
||||||
|
slots--;
|
||||||
|
const { scanlator, title} = manga;
|
||||||
|
const List = manga.List.sort((a, b) => { return a.num - b.num });
|
||||||
|
for (let i = 0; i < List.length; i++)
|
||||||
|
{
|
||||||
|
const ch = List[i];
|
||||||
|
if (isNaN(ch.num)) continue;
|
||||||
|
const { num, link } = ch;
|
||||||
|
const chapter = await new Chapter(scanlator, link, title, num).get();
|
||||||
|
// console.log(chapter)
|
||||||
|
if (!chapter?.List?.length) continue;
|
||||||
|
const dlChapter = await downloadChapter(chapter);
|
||||||
|
if(dlChapter=='UserStoped') break;
|
||||||
|
}
|
||||||
|
Queue.shift();
|
||||||
|
slots++;
|
||||||
|
Emitter.emit('queueUpdate', '');
|
||||||
|
}
|
||||||
|
async function downloadChapter(chapter)
|
||||||
|
{
|
||||||
|
const {List, scanlator, title, chNum } = chapter;
|
||||||
|
const images = List;
|
||||||
|
for (let i = 0; i < images.length; i++) {
|
||||||
|
const img = images[i];
|
||||||
|
const destination = `./public/Saved/${scanlator}/${title}/CH_${chNum}/${img.split('/')[(img.split('/').length - 1)]}`;
|
||||||
|
try {
|
||||||
|
const downloaded = await downloadImage(img, destination);
|
||||||
|
console.log(`Downloaded: Scanlator:${scanlator}; Manga: ${title} Chapter:${chNum}; IMG:${img.split('/')[(img.split('/').length - 1)]} ${downloaded.status}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error downloading: ${img}: ${error}`);
|
||||||
|
if(!shouldKeepOnDownloading)
|
||||||
|
{
|
||||||
|
ReturnError = error.Error;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !shouldKeepOnDownloading?'UserStoped':{chNum};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Link} url
|
||||||
|
* @param {Path} destPath
|
||||||
|
* @param {Number} maxRetries
|
||||||
|
* @returns {Download}
|
||||||
|
* @typedef {String} Link
|
||||||
|
* @typedef {Object} Download
|
||||||
|
* @property {String} status
|
||||||
|
* @property {Path} file
|
||||||
|
*/
|
||||||
|
async function downloadImage(url, destPath, maxRetries = 3)
|
||||||
|
{
|
||||||
|
let retries = 0;
|
||||||
|
if(!shouldKeepOnDownloading) throw new Error('UserStoped');
|
||||||
|
while (retries < maxRetries && shouldKeepOnDownloading) {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const response = await fetch(url);
|
||||||
|
const dir = path.dirname(destPath);
|
||||||
|
if (!response.ok) throw new Error(`Failed to download image. Status Code: ${response.status}`);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
const fileStream = fs.createWriteStream(destPath);
|
||||||
|
await new Promise((resolve, reject) =>
|
||||||
|
{
|
||||||
|
const stream = Stream.Readable.fromWeb(response.body).pipe(fileStream);
|
||||||
|
stream.on('error', reject);
|
||||||
|
fileStream.on('finish', () => {
|
||||||
|
fileStream.close();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return {status:'Downloaded', file:destPath}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error downloading image: ${destPath}, Retry ${retries + 1}/${maxRetries}`);//, error);
|
||||||
|
retries++;
|
||||||
|
if(retries == maxRetries)
|
||||||
|
return {status:'Failed', file:destPath}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,478 @@
|
|||||||
|
const find = require('findit');
|
||||||
|
const path = require('path');
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
module.exports.Modules = class Modules
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} name Scanlator Name - Same as in the Module.
|
||||||
|
*/
|
||||||
|
constructor (name=null)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.Modules;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @returns {Array<Modules>}
|
||||||
|
* @typedef {Class} Modules
|
||||||
|
*/
|
||||||
|
getAll()
|
||||||
|
{
|
||||||
|
let finder = find(path.resolve('Modules'));
|
||||||
|
finder.on('file', file =>
|
||||||
|
{
|
||||||
|
let Module = require(file);
|
||||||
|
if (Module.constructor)
|
||||||
|
{
|
||||||
|
this.Modules.push(Module);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
finder.on('end', () =>
|
||||||
|
{
|
||||||
|
return {Modules:this.Modules}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
async exists()
|
||||||
|
{
|
||||||
|
if (!this.name) return false;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
{
|
||||||
|
const finder = find(path.resolve('Modules'));
|
||||||
|
finder.on('file', async file =>
|
||||||
|
{
|
||||||
|
let Module = require(file);
|
||||||
|
if (Module.constructor)
|
||||||
|
{
|
||||||
|
const test = new Module();
|
||||||
|
if (this.name == test.scanlator)
|
||||||
|
{
|
||||||
|
finder.removeAllListeners();
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
finder.on('error', err =>
|
||||||
|
{
|
||||||
|
reject(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
finder.on('end', () =>
|
||||||
|
{
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} title
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
async titleExists(title)
|
||||||
|
{
|
||||||
|
if(!this.name) return false;
|
||||||
|
const module = require(`./Modules/${this.name.split('-')[0]+'Module'}`);
|
||||||
|
return await new module().hasTitle(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.Search = class Search
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} searchString - String to search by.
|
||||||
|
* @param {Scanlator} scanlator - Scanlator Name
|
||||||
|
* @typedef {String} Scanlator
|
||||||
|
* @pattern /^[\w-]+-scans$/
|
||||||
|
*/
|
||||||
|
constructor(searchString, scanlator=null)
|
||||||
|
{
|
||||||
|
this.ss = searchString;
|
||||||
|
this.scanlator = scanlator;
|
||||||
|
this.Modules = [];
|
||||||
|
this.results = [];
|
||||||
|
this.initialized = new Promise((resolve) =>
|
||||||
|
{
|
||||||
|
let finder = find(path.resolve('Modules'));
|
||||||
|
finder.on('file', file =>
|
||||||
|
{
|
||||||
|
let Module = require(file);
|
||||||
|
if (Module.constructor)
|
||||||
|
{
|
||||||
|
this.Modules.push(Module);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
finder.on('end', () =>
|
||||||
|
{
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Array<Results>}
|
||||||
|
* @typedef {Object} Results
|
||||||
|
* @property {Array<Manga>} Results
|
||||||
|
* @property {Scanlator} scanlator
|
||||||
|
* @typedef {Object} Manga
|
||||||
|
* @property {String} link - Manga Link
|
||||||
|
* @property {String} title - Manga Title
|
||||||
|
* @property {String} img - Image Link
|
||||||
|
* @typedef {String} Scanlator
|
||||||
|
* @pattern /^[\w-]+-scans$/
|
||||||
|
*/
|
||||||
|
async search()
|
||||||
|
{
|
||||||
|
await this.initialized;
|
||||||
|
if(this.Modules.length==0) return
|
||||||
|
for(const module of this.Modules)
|
||||||
|
{
|
||||||
|
const auxModule = new module()
|
||||||
|
if(!this.scanlator || this.scanlator == auxModule.scanlator )
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var results = await auxModule.Search(this.ss);
|
||||||
|
//TODO: Defaults for missing info from the modules;
|
||||||
|
// console.log('Lib: Search: search method: result:', result);
|
||||||
|
results.sort((a, b) => {
|
||||||
|
if (a.title.toUpperCase() < b.title.toUpperCase()) return -1;
|
||||||
|
else return 1;
|
||||||
|
});
|
||||||
|
for(var i = 0; i< results.length; i++)
|
||||||
|
{
|
||||||
|
results[i]['Status'] = results[i].status;
|
||||||
|
}
|
||||||
|
this.results.push({
|
||||||
|
Results:results,
|
||||||
|
scanlator:auxModule.scanlator
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error('Lib: Module Errored:', auxModule.scanlator, 'Error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.results
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
module.exports.SearchByTag = class SearchByTag
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} tag
|
||||||
|
*/
|
||||||
|
constructor(tag)
|
||||||
|
{
|
||||||
|
this.ss = tag.toLowerCase();
|
||||||
|
this.Modules = [];
|
||||||
|
this.results = [];
|
||||||
|
this.initialized = new Promise((resolve) =>
|
||||||
|
{
|
||||||
|
let finder = find(path.resolve('Modules'));
|
||||||
|
finder.on('file', file =>
|
||||||
|
{
|
||||||
|
let Module = require(file);
|
||||||
|
if (Module.constructor)
|
||||||
|
{
|
||||||
|
this.Modules.push(Module);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
finder.on('end', () =>
|
||||||
|
{
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Number} ammount - 5 by default
|
||||||
|
* @returns {Array<Results>}
|
||||||
|
* @typedef {Object} Results
|
||||||
|
* @property {Array<Manga>} Results
|
||||||
|
* @property {Scanlator} scanlator
|
||||||
|
* @typedef {Object} Manga
|
||||||
|
* @property {String} link - Manga Link
|
||||||
|
* @property {String} title - Manga Title
|
||||||
|
* @property {String} img - Image Link
|
||||||
|
* @typedef {String} Scanlator
|
||||||
|
* @pattern /^[\w-]+-scans$/
|
||||||
|
*/
|
||||||
|
async search(ammount=5)
|
||||||
|
{
|
||||||
|
await this.initialized;
|
||||||
|
if(this.Modules.length==0) return
|
||||||
|
for(const module of this.Modules)
|
||||||
|
{
|
||||||
|
const auxModule = new module()
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var results = await auxModule.SearchByTag(this.ss, ammount);
|
||||||
|
//TODO: Defaults for missing info from the modules;
|
||||||
|
// console.log('Lib: Search: search method: result:', results);
|
||||||
|
results.sort((a, b) => {
|
||||||
|
if (a.title.toUpperCase() < b.title.toUpperCase()) return -1;
|
||||||
|
else return 1;
|
||||||
|
});
|
||||||
|
for(var i = 0; i< results.length; i++)
|
||||||
|
{
|
||||||
|
results[i]['Status'] = results[i].status;
|
||||||
|
}
|
||||||
|
this.results.push({
|
||||||
|
Results:results,
|
||||||
|
scanlator:auxModule.scanlator
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error('Lib: Module Errored:', auxModule.scanlator, 'Error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.Manga = class Manga
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Scanlator} scanlator
|
||||||
|
* @param {Link} link Manga Link
|
||||||
|
* @param {String} title
|
||||||
|
* @typedef {String} Link
|
||||||
|
* @pattern ^(https?:\/\/)?([\w\d.-]+)\.([a-z]{2,})(:[0-9]+)?(\/\S*)?$
|
||||||
|
* @typedef {String} Scanlator
|
||||||
|
* @pattern /^[\w-]+-scans$/
|
||||||
|
*/
|
||||||
|
constructor(scanlator, link, title)
|
||||||
|
{
|
||||||
|
this.scanlator = scanlator;
|
||||||
|
this.link = link;
|
||||||
|
this.title = title;
|
||||||
|
this.Engine;
|
||||||
|
this.initialized = new Promise((resolve) =>
|
||||||
|
{
|
||||||
|
let finder = find(path.resolve('Modules'));
|
||||||
|
finder.on('file', file =>
|
||||||
|
{
|
||||||
|
let Module = require(file);
|
||||||
|
if (Module.constructor)
|
||||||
|
{
|
||||||
|
if(this.scanlator == new Module().scanlator)
|
||||||
|
{
|
||||||
|
this.Engine = Module;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Manga}
|
||||||
|
* @typedef {Object} Manga
|
||||||
|
* @property {String} link - Manga Link
|
||||||
|
* @property {String} title - Manga Title
|
||||||
|
* @property {String} img - Image Link
|
||||||
|
* @property {Array<String>} tags - Manga Tags
|
||||||
|
* @property {Array<Chapter>} List - Chapter List
|
||||||
|
* @typedef {Object} Chapter
|
||||||
|
* @property {Number} num - Chapter Number
|
||||||
|
* @property {Link} link - Chapter Link
|
||||||
|
* @property {Date_} date - Chapter uploaded date
|
||||||
|
* @typedef {String} Link
|
||||||
|
* @typedef {String} Date_
|
||||||
|
*/
|
||||||
|
async get()
|
||||||
|
{
|
||||||
|
await this.initialized;
|
||||||
|
try {
|
||||||
|
const Scanlator = new this.Engine();
|
||||||
|
const Manga = await Scanlator.GetManga(this.link, this.title);
|
||||||
|
const List = await Scanlator.ChapterList(this.link, this.title);
|
||||||
|
const Status = Manga.Status?Manga.Status:Manga.status;
|
||||||
|
//TODO: Defaults for missing info from the modules;
|
||||||
|
return {...Manga, Status, link:this.link, scanlator:this.scanlator, List, title:this.title}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Lib: Manga: Error:' ,error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.Chapter = class Chapter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Scanlator} scanlator - Scanlator Name
|
||||||
|
* @param {Link} Link - Chapter Link
|
||||||
|
* @param {String} title - Manga Title
|
||||||
|
* @param {Number} chNum - Chapter Number
|
||||||
|
* @typedef {String} Link
|
||||||
|
* @pattern ^(https?:\/\/)?([\w\d.-]+)\.([a-z]{2,})(:[0-9]+)?(\/\S*)?$
|
||||||
|
* @typedef {String} Scanlator
|
||||||
|
* @pattern /^[\w-]+-scans$/
|
||||||
|
*/
|
||||||
|
constructor(scanlator, Link,title, chNum)
|
||||||
|
{
|
||||||
|
this.scanlator = scanlator;
|
||||||
|
this.title = title;
|
||||||
|
this.Link = Link;
|
||||||
|
this.chNum = chNum;
|
||||||
|
this.Engine;
|
||||||
|
this.initialized = new Promise((resolve) =>
|
||||||
|
{
|
||||||
|
let finder = find(path.resolve('Modules'));
|
||||||
|
finder.on('file', file =>
|
||||||
|
{
|
||||||
|
let Module = require(file);
|
||||||
|
if (Module.constructor)
|
||||||
|
{
|
||||||
|
if(this.scanlator == new Module().scanlator)
|
||||||
|
{
|
||||||
|
this.Engine = Module;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Chapter}
|
||||||
|
* @typedef {Object} Chapter
|
||||||
|
* @property {Manga} Manga - Manga Link
|
||||||
|
* @property {Array<Img>} List - Chapter List of images
|
||||||
|
* @property {Number} chNum - Chapter number
|
||||||
|
* @typedef {Object} Manga
|
||||||
|
* @property {String} link - Manga Link
|
||||||
|
* @property {String} title - Manga Title
|
||||||
|
* @property {String} img - Image Link
|
||||||
|
* @typedef {String} Link
|
||||||
|
* @typedef {Link} Img
|
||||||
|
*/
|
||||||
|
async get()
|
||||||
|
{
|
||||||
|
await this.initialized;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const Scanlator = new this.Engine();
|
||||||
|
const {List, mangaLink} = await Scanlator.Chapter(this.Link);
|
||||||
|
const {latestChap } = await Scanlator.GetManga(mangaLink);
|
||||||
|
//TODO: Defaults for missing info from the modules;
|
||||||
|
return {List, latestChap,mangaLink, chNum:parseInt(this.chNum), link:this.Link,title:this.title, scanlator:this.scanlator}
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error('Lib: Chapter: Error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getMangaLink()
|
||||||
|
{
|
||||||
|
await this.initialized;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const Scanlator = new this.Engine();
|
||||||
|
const {mangaLink} = await Scanlator.Chapter(this.Link);
|
||||||
|
return mangaLink;
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error('Lib: Chapter: Error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
exports.decodeHTML = (encodedString)=>
|
||||||
|
{
|
||||||
|
return encodedString.replace(/ |&#(\d+);|\n/g, function(match, dec)
|
||||||
|
{
|
||||||
|
if (dec)
|
||||||
|
{
|
||||||
|
return String.fromCharCode(dec); // Replace HTML character codes
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ''; // Remove newline characters
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.isValidID = (id) =>
|
||||||
|
{
|
||||||
|
return mongoose.Types.ObjectId.isValid(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @argument {Response} res - Response: express response object.
|
||||||
|
* @argument {String} name - Name of the cookie.
|
||||||
|
* @argument {*} value - Value of the cookie.
|
||||||
|
*/
|
||||||
|
exports.Baker = async (res, name, value, maxAge =2) =>
|
||||||
|
{
|
||||||
|
await res.cookie(name, '',{maxAge:0});
|
||||||
|
await res.cookie(name, value, {maxAge:hours(2),path:'/',secure:false,httpOnly:false});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Use it to transform an hour value into miliseconds.
|
||||||
|
* @param {Number} Hours - Hours to transform
|
||||||
|
*/
|
||||||
|
function hours(Hours)
|
||||||
|
{
|
||||||
|
return Hours*60*60*1000;
|
||||||
|
}
|
||||||
|
exports.Crypto = class Crypto
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} username
|
||||||
|
* @param {String} password
|
||||||
|
*/
|
||||||
|
constructor(username, password)
|
||||||
|
{
|
||||||
|
this.salt = '4$3H6Q9B8d4VXcE3Rft4';
|
||||||
|
this.secret = '7xJYrX@KF7oSNr9Zm4UH';
|
||||||
|
this.pepper = String.fromCharCode(this.getRandomInt(65, 90));
|
||||||
|
this.password = password;
|
||||||
|
this.user = username;
|
||||||
|
this.algorithm = 'aes-192-cbc';
|
||||||
|
|
||||||
|
}
|
||||||
|
get Hash() // to use on register
|
||||||
|
{
|
||||||
|
return this.hash()
|
||||||
|
}
|
||||||
|
hash(p)
|
||||||
|
{
|
||||||
|
if(!p)
|
||||||
|
{
|
||||||
|
return crypto.createHmac('sha256', this.secret).update(this.user+this.password+this.salt+this.pepper).digest('hex');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return crypto.createHmac('sha256', this.secret).update(this.user+this.password+this.salt+p).digest('hex');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getRandomInt(min, max)
|
||||||
|
{
|
||||||
|
min = Math.ceil(min);
|
||||||
|
max = Math.floor(max);
|
||||||
|
return Math.floor(Math.random() * (max - min)) + min;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {String} cookie
|
||||||
|
* @param {String} name
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
exports.cookieParser = (cookie, name)=>
|
||||||
|
{
|
||||||
|
return cookie.split(name+'=')[1].split(';')[0];
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
let favorite =
|
||||||
|
new Schema(
|
||||||
|
{
|
||||||
|
scanlator: {type: String, required: true, max: 100},
|
||||||
|
title: {type: String, required: true, max: 100},
|
||||||
|
link:{type: String, require:true, max:100},
|
||||||
|
tags:{type: Array, require:false}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Favorite = module.exports = mongoose.model('favorite', favorite, 'mangareader');
|
||||||
|
module.exports.get = (callback, limit)=>
|
||||||
|
{
|
||||||
|
Favorite.find(callback).limit(limit);
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
let chapter =
|
||||||
|
new Schema(
|
||||||
|
{
|
||||||
|
scanlator: {type: String, required: true, max: 100},
|
||||||
|
title: {type: String, required: true, max: 100},
|
||||||
|
link:{type: String, require:true, max:100},
|
||||||
|
chapterNum:{type: Number, require:true}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Chapter = module.exports = mongoose.model('chapter', chapter, 'mangareader');
|
||||||
|
module.exports.get = (callback, limit)=>
|
||||||
|
{
|
||||||
|
Chapter.find(callback).limit(limit);
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
let user =
|
||||||
|
new Schema(
|
||||||
|
{
|
||||||
|
username: {type: String, required: true, max: 100},
|
||||||
|
hash: {type: String, required: true, max: 100}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const User = module.exports = mongoose.model('favorite', user, 'mangareader');
|
||||||
|
module.exports.get = (callback, limit)=>
|
||||||
|
{
|
||||||
|
User.find(callback).limit(limit);
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"ignore": ["./config.json"]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "masterhcsite",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "masterhcsite",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "nodemon test.js",
|
||||||
|
"start": "node server.js",
|
||||||
|
"devStart": "nodemon --env-file=.env server.js"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"ejs": "^3.1.9",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"findit": "^2.0.0",
|
||||||
|
"mongoose": "^8.1.3",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"node-html-parser": "^6.1.12",
|
||||||
|
"nodemon": "^3.0.3",
|
||||||
|
"sharp": "^0.33.3",
|
||||||
|
"three": "^0.161.0"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,899 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@1,900&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css?family=Roboto+Slab:100,300,400,700');
|
||||||
|
@import url('https://fonts.googleapis.com/css?family=Raleway:300,300i,400,400i,500,500i,600,600i,700,700i,800,800i,900,900i');
|
||||||
|
:root
|
||||||
|
{
|
||||||
|
|
||||||
|
--purple-heart-50: #f3f1ff;
|
||||||
|
--purple-heart-100: #ebe5ff;
|
||||||
|
--purple-heart-200: #d9ceff;
|
||||||
|
--purple-heart-300: #bea6ff;
|
||||||
|
--purple-heart-400: #9f75ff;
|
||||||
|
--purple-heart-500: #843dff;
|
||||||
|
--purple-heart-600: #7916ff;
|
||||||
|
--purple-heart-700: #6b04fd;
|
||||||
|
--purple-heart-800: #5a03d5;
|
||||||
|
--purple-heart-900: #4b05ad;
|
||||||
|
--purple-heart-950: #2c0076;
|
||||||
|
/* Colors with 25% transparency */
|
||||||
|
--purple-heart-50-25: #f3f1ff40;
|
||||||
|
--purple-heart-100-25: #ebe5ff40;
|
||||||
|
--purple-heart-200-25: #d9ceff40;
|
||||||
|
--purple-heart-300-25: #bea6ff40;
|
||||||
|
--purple-heart-400-25: #9f75ff40;
|
||||||
|
--purple-heart-500-25: #843dff40;
|
||||||
|
--purple-heart-600-25: #7916ff40;
|
||||||
|
--purple-heart-700-25: #6b04fd40;
|
||||||
|
--purple-heart-800-25: #5a03d540;
|
||||||
|
--purple-heart-900-25: #4b05ad40;
|
||||||
|
--purple-heart-950-25: #2c007640;
|
||||||
|
|
||||||
|
/* Colors with 50% transparency */
|
||||||
|
--purple-heart-50-50: #f3f1ff80;
|
||||||
|
--purple-heart-100-50: #ebe5ff80;
|
||||||
|
--purple-heart-200-50: #d9ceff80;
|
||||||
|
--purple-heart-300-50: #bea6ff80;
|
||||||
|
--purple-heart-400-50: #9f75ff80;
|
||||||
|
--purple-heart-500-50: #843dff80;
|
||||||
|
--purple-heart-600-50: #7916ff80;
|
||||||
|
--purple-heart-700-50: #6b04fd80;
|
||||||
|
--purple-heart-800-50: #5a03d580;
|
||||||
|
--purple-heart-900-50: #4b05ad80;
|
||||||
|
--purple-heart-950-50: #2c007680;
|
||||||
|
|
||||||
|
--bg:var(--bgBetter);
|
||||||
|
--bgBetter:linear-gradient(135deg, var(--purple-heart-400) 0%, var(--purple-heart-800) 100%);
|
||||||
|
--lbg:var(--purple-heart-600);
|
||||||
|
--lbgT:var(--purple-heart-600-50);
|
||||||
|
--primary:var(--purple-heart-600);
|
||||||
|
--accent:var(--purple-heart-800);
|
||||||
|
--border: var(--purple-heart-500);
|
||||||
|
--border-hover: var(--purple-heart-300);
|
||||||
|
--text:var(--purple-heart-100);
|
||||||
|
--favorite:var(--purple-heart-950);
|
||||||
|
--completed: var(--purple-heart-400);
|
||||||
|
--dropped: #de3b3b;
|
||||||
|
|
||||||
|
}
|
||||||
|
body
|
||||||
|
{
|
||||||
|
margin:0;
|
||||||
|
height:100%;
|
||||||
|
width:100%;
|
||||||
|
margin: 0 0 0 0;
|
||||||
|
position:absolute;
|
||||||
|
top:0%;
|
||||||
|
left:0%;
|
||||||
|
font-family: 'lato';
|
||||||
|
background-color: var(--bg);
|
||||||
|
background-image: linear-gradient(135deg, var(--purple-heart-400) 0%, var(--purple-heart-950) 100%);;
|
||||||
|
background: -webkit-gradient(linear, left top, right bottom, from(var(--purple-heart-400)), to(var(--purple-heart-950))) fixed;
|
||||||
|
color:var(--text);
|
||||||
|
overflow: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.nav
|
||||||
|
{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.nav > .button-container
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap:2em;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container > .active
|
||||||
|
{
|
||||||
|
color:var(--accent);
|
||||||
|
}
|
||||||
|
.bt
|
||||||
|
{
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
.bt:hover
|
||||||
|
{
|
||||||
|
color:var(--accent)
|
||||||
|
}
|
||||||
|
button
|
||||||
|
{
|
||||||
|
cursor: pointer;
|
||||||
|
width: 10em;
|
||||||
|
border:2px solid var(--border);
|
||||||
|
border-radius: 15px;
|
||||||
|
font-family: 'lato';
|
||||||
|
color: var(--text);
|
||||||
|
background-color: var(--purple-heart-800);
|
||||||
|
}
|
||||||
|
button:hover
|
||||||
|
{
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
}
|
||||||
|
#display
|
||||||
|
{
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container
|
||||||
|
{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search
|
||||||
|
{
|
||||||
|
background-color: var(--purple-heart-950);
|
||||||
|
border: 2px solid var(--text);
|
||||||
|
border-radius: 15px;
|
||||||
|
height: 2em;
|
||||||
|
text-align: center;
|
||||||
|
color:var(--text);
|
||||||
|
|
||||||
|
}
|
||||||
|
#search:focus-visible
|
||||||
|
{
|
||||||
|
background-color: var(--purple-heart-950);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 15px;
|
||||||
|
height: 2em;
|
||||||
|
text-align: center;
|
||||||
|
color:var(--text);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
transform: translateX(-1em);
|
||||||
|
cursor: pointer;
|
||||||
|
color:var(--accent);
|
||||||
|
font-variation-settings:
|
||||||
|
'FILL' 0,
|
||||||
|
'wght' 400,
|
||||||
|
'GRAD' 0,
|
||||||
|
'opsz' 24
|
||||||
|
|
||||||
|
}
|
||||||
|
.fill
|
||||||
|
{
|
||||||
|
cursor: pointer;
|
||||||
|
color:var(--favorite);
|
||||||
|
font-variation-settings:
|
||||||
|
'FILL' 1,
|
||||||
|
'wght' 400,
|
||||||
|
'GRAD' 0,
|
||||||
|
'opsz' 24
|
||||||
|
}
|
||||||
|
.scanlator-container > h2
|
||||||
|
{
|
||||||
|
margin-left: 3em;
|
||||||
|
margin-right: 3em;
|
||||||
|
border-bottom:4px solid var(--border-hover);
|
||||||
|
}
|
||||||
|
.carousel > .buttonContainer
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 2em;
|
||||||
|
height: 3em;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
z-index: 5;
|
||||||
|
top: -15.5em;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.carousel
|
||||||
|
{
|
||||||
|
height: 25em;
|
||||||
|
align-items: center;
|
||||||
|
width: 90%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.cards-container
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
background-color: var(--purple-heart-800-25);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 12px;
|
||||||
|
height: 100%;
|
||||||
|
gap:2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron
|
||||||
|
{
|
||||||
|
transform: translateX(0em);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text);
|
||||||
|
font-variation-settings:
|
||||||
|
'FILL' 0,
|
||||||
|
'wght' 400,
|
||||||
|
'GRAD' 0,
|
||||||
|
'opsz' 24
|
||||||
|
|
||||||
|
}
|
||||||
|
.pullBackUp
|
||||||
|
{
|
||||||
|
position: fixed;
|
||||||
|
right: 2em;
|
||||||
|
bottom: 2em;
|
||||||
|
color:var(--text);
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
.chevronUp
|
||||||
|
{
|
||||||
|
color: var(---text);
|
||||||
|
margin-left: 0.73em;
|
||||||
|
top: -0.07em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.prev-btn, .next-btn
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
margin: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--accent);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 3em;
|
||||||
|
width: 3em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
/* align-content: center; */
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card
|
||||||
|
{
|
||||||
|
transition: all .4s cubic-bezier(0.175, 0.885, 0, 1);
|
||||||
|
background-color: var(--lbg);
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 15%;
|
||||||
|
height: 98.5%;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0px 13px 10px -7px rgba(0, 0, 0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark
|
||||||
|
{
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__img
|
||||||
|
{
|
||||||
|
transition: 0.2s all ease-out;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
height: 75%;
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__info
|
||||||
|
{
|
||||||
|
background-color:var(--lbg);
|
||||||
|
border-bottom-left-radius: 12px;
|
||||||
|
border-bottom-right-radius: 12px;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__title {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: 'lato', serif;
|
||||||
|
height: 2.6em;
|
||||||
|
font-size: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover > .card__info
|
||||||
|
{
|
||||||
|
background-color: var(--lbgT);
|
||||||
|
}
|
||||||
|
.card:hover > .card__img
|
||||||
|
{
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.infoCard
|
||||||
|
{
|
||||||
|
height: 15em;
|
||||||
|
width: 80%;
|
||||||
|
display: flex;
|
||||||
|
border: 3px solid var(--border);
|
||||||
|
border-radius: 15px;
|
||||||
|
margin-left: 10%;
|
||||||
|
background-color: var(--lbg);
|
||||||
|
}
|
||||||
|
.infoCard > .infoText
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
.infoCard > .infoText > h2
|
||||||
|
{
|
||||||
|
margin-top: 0.3em;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
.infoCard > .infoText > .description
|
||||||
|
{
|
||||||
|
height: 8em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.infoCard > img
|
||||||
|
{
|
||||||
|
border-radius: 5px;
|
||||||
|
margin:10px;
|
||||||
|
}
|
||||||
|
.infoCard > .infoText > a
|
||||||
|
{
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.infoCard > .infoText > .description
|
||||||
|
{
|
||||||
|
color:var(--text);
|
||||||
|
}
|
||||||
|
.list
|
||||||
|
{
|
||||||
|
width: 80%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
border: 3px solid var(--border);
|
||||||
|
background-color: var(--bg);
|
||||||
|
border-radius: 15px;
|
||||||
|
margin-left: 10%;
|
||||||
|
gap:5px;
|
||||||
|
}
|
||||||
|
.list > .chapterButton
|
||||||
|
{
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: var(--lbg);
|
||||||
|
margin: 5px;
|
||||||
|
width: 130px;
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.chapterButton:hover
|
||||||
|
{
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
}
|
||||||
|
.list > .chapterButton > a
|
||||||
|
{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list > .chapterButton > a > h4 , .list > .chapterButton > a > h5
|
||||||
|
{
|
||||||
|
/* margin-left: 0.6em; */
|
||||||
|
margin-bottom: 0.1em;
|
||||||
|
margin-top: 0.1em;
|
||||||
|
}
|
||||||
|
.infoCard > .infoText > .tags
|
||||||
|
{
|
||||||
|
display:flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: left;
|
||||||
|
align-self: center;
|
||||||
|
gap:5px;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
.infoCard > .infoText > .tags > .tag
|
||||||
|
{
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: var(--accent);
|
||||||
|
padding-left: 0.2em;
|
||||||
|
padding-right: .2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#display > img
|
||||||
|
{
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
.chapterNav
|
||||||
|
{
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
.chapterNavButton
|
||||||
|
{
|
||||||
|
background-color: var(--purple-heart-400-50);
|
||||||
|
width: 100px;
|
||||||
|
height: 30px;
|
||||||
|
border: 2px solid var(--accent);
|
||||||
|
border-radius: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.chapterNavButton:hover
|
||||||
|
{
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
}
|
||||||
|
.searchCarousel
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
.chapterNavChapterLabel
|
||||||
|
{
|
||||||
|
width: 40%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.hidden
|
||||||
|
{
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.searchBarWrapper
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2em;
|
||||||
|
}
|
||||||
|
.searchIcon
|
||||||
|
{
|
||||||
|
color:var(--text)
|
||||||
|
}
|
||||||
|
.searchIcon:hover
|
||||||
|
{
|
||||||
|
color:var(--accent)
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
height: 4px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-clip: padding-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress .indeterminate:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--accent);
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
will-change: left, right;
|
||||||
|
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395)
|
||||||
|
infinite;
|
||||||
|
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
|
||||||
|
}
|
||||||
|
.progress .indeterminate:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--accent);
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
will-change: left, right;
|
||||||
|
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
|
||||||
|
infinite;
|
||||||
|
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
|
||||||
|
infinite;
|
||||||
|
-webkit-animation-delay: 1.15s;
|
||||||
|
animation-delay: 1.15s;
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.htmx-request .progress {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.htmx-request.progress {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes indeterminate {
|
||||||
|
0% {
|
||||||
|
left: -35%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
left: 100%;
|
||||||
|
right: -90%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
right: -90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes indeterminate {
|
||||||
|
0% {
|
||||||
|
left: -35%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
left: 100%;
|
||||||
|
right: -90%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
right: -90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes indeterminate-short {
|
||||||
|
0% {
|
||||||
|
left: -200%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
left: 107%;
|
||||||
|
right: -8%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 107%;
|
||||||
|
right: -8%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes indeterminate-short {
|
||||||
|
0% {
|
||||||
|
left: -200%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
left: 107%;
|
||||||
|
right: -8%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 107%;
|
||||||
|
right: -8%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#RecommendTitle
|
||||||
|
{
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2em;
|
||||||
|
color: var(--purple-heart-50);
|
||||||
|
border-bottom: 3px solid var(--purple-heart-50);
|
||||||
|
width: 80%;
|
||||||
|
left: 10%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chapterNavChapterLabel
|
||||||
|
{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.chapterNavChapterLabel:hover
|
||||||
|
{
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status
|
||||||
|
{
|
||||||
|
position: absolute;
|
||||||
|
top: 2em;
|
||||||
|
left: -3.5em;
|
||||||
|
color: var(--text);
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.5em 4em;
|
||||||
|
background: var(--dropped);
|
||||||
|
-ms-transform: rotate(-45deg);
|
||||||
|
-webkit-transform: rotate(-45deg);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
.Completed
|
||||||
|
{
|
||||||
|
background: var(--completed);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--border-hover)
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background:var(--purple-heart-950-50);
|
||||||
|
border: 0 solid transparent;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background:var(--purple-heart-950) ; /* color of the thumb on hover */
|
||||||
|
}
|
||||||
|
.NoFavs
|
||||||
|
{
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
.login
|
||||||
|
{
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: var(--purple-heart-500);
|
||||||
|
height: 2em;
|
||||||
|
width: 4em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
top:1em;
|
||||||
|
right: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.login:hover
|
||||||
|
{
|
||||||
|
background-color: var(--purple-heart-600);
|
||||||
|
}
|
||||||
|
#configWrapper
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.configForm
|
||||||
|
{
|
||||||
|
background-color: var(--purple-heart-600-50);
|
||||||
|
border-radius: 13px;
|
||||||
|
height:25em;
|
||||||
|
}
|
||||||
|
.configForm section
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.buttonSection
|
||||||
|
{
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
#configSubmitBt
|
||||||
|
{
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
.configForm label, .configForm input
|
||||||
|
{
|
||||||
|
margin: 1em 0 0 1em;
|
||||||
|
width: 30vw;
|
||||||
|
}
|
||||||
|
.configForm::before
|
||||||
|
{
|
||||||
|
content: '';
|
||||||
|
background-color: var(--accent);
|
||||||
|
border-top-left-radius: 13px;
|
||||||
|
border-bottom-left-radius: 13px;
|
||||||
|
z-index: 10;
|
||||||
|
height: 100%;
|
||||||
|
width: 1em;
|
||||||
|
position: relative;
|
||||||
|
float:left;
|
||||||
|
}
|
||||||
|
.configForm input
|
||||||
|
{
|
||||||
|
background-color: var(--purple-heart-950);
|
||||||
|
border: 2px solid var(--text);
|
||||||
|
border-radius: 15px;
|
||||||
|
height: 2em;
|
||||||
|
text-align: center;
|
||||||
|
color:var(--text);
|
||||||
|
margin-right: 2em;
|
||||||
|
|
||||||
|
}
|
||||||
|
.configForm input:focus-visible
|
||||||
|
{
|
||||||
|
background-color: var(--purple-heart-950);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 15px;
|
||||||
|
height: 2em;
|
||||||
|
text-align: center;
|
||||||
|
color:var(--text);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formWrapper
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.loginForm
|
||||||
|
{
|
||||||
|
background-color: var(--purple-heart-600-50);
|
||||||
|
border-radius: 13px;
|
||||||
|
height:25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginForm section
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.loginForm label, .loginForm input
|
||||||
|
{
|
||||||
|
margin: 1em 0 0 1em;
|
||||||
|
width: 30vw;
|
||||||
|
}
|
||||||
|
.loginForm::before
|
||||||
|
{
|
||||||
|
content: '';
|
||||||
|
background-color: var(--accent);
|
||||||
|
border-top-left-radius: 13px;
|
||||||
|
border-bottom-left-radius: 13px;
|
||||||
|
z-index: 10;
|
||||||
|
height: 100%;
|
||||||
|
width: 1em;
|
||||||
|
position: relative;
|
||||||
|
float:left;
|
||||||
|
}
|
||||||
|
.loginForm input
|
||||||
|
{
|
||||||
|
background-color: var(--purple-heart-950);
|
||||||
|
border: 2px solid var(--text);
|
||||||
|
border-radius: 15px;
|
||||||
|
height: 2em;
|
||||||
|
text-align: center;
|
||||||
|
color:var(--text);
|
||||||
|
margin-right: 2em;
|
||||||
|
|
||||||
|
}
|
||||||
|
.loginForm input:focus-visible
|
||||||
|
{
|
||||||
|
background-color: var(--purple-heart-950);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 15px;
|
||||||
|
height: 2em;
|
||||||
|
text-align: center;
|
||||||
|
color:var(--text);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginFormFlexBox
|
||||||
|
{
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap:2em;
|
||||||
|
}
|
||||||
|
#loginSubmitBt
|
||||||
|
{
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
.burgerMenu > span
|
||||||
|
{
|
||||||
|
color:var(--text);
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
.wrapperReaderBox
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.spacer
|
||||||
|
{
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
.chapterOverview
|
||||||
|
{
|
||||||
|
position: fixed;
|
||||||
|
height: 85vh;
|
||||||
|
top: 10vh;
|
||||||
|
width: 1.7vw;
|
||||||
|
background-color: var(--purple-heart-500-50);
|
||||||
|
border: 0 solid;
|
||||||
|
border-radius: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.chapterOverview > .flexBox
|
||||||
|
{
|
||||||
|
display: none;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5em;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow-x: hidden;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
.chapterOverview > .expand
|
||||||
|
{
|
||||||
|
color:var(--text);
|
||||||
|
border:0 solid transparent;
|
||||||
|
border-top-right-radius: 15px ;
|
||||||
|
border-bottom-right-radius: 15px ;
|
||||||
|
background-color: var(--purple-heart-600);
|
||||||
|
width: 1.65vw;
|
||||||
|
cursor:pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
.chapterOverview > .expand > span
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
left: 1em;
|
||||||
|
top: 50%;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.chapterOverview > .expand:hover
|
||||||
|
{
|
||||||
|
background-color: var(--purple-heart-400);
|
||||||
|
|
||||||
|
}
|
||||||
|
.flexBox > img
|
||||||
|
{
|
||||||
|
max-width: 75px;
|
||||||
|
height: auto;
|
||||||
|
border: 0 solid transparent;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
function triggerInputClick() {
|
||||||
|
var inputElement = document.getElementById('search');
|
||||||
|
inputElement.click();
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
const router = require('express').Router();
|
||||||
|
const api = require('../controllers/api.js');
|
||||||
|
|
||||||
|
module.exports = (worker)=>
|
||||||
|
{
|
||||||
|
router.route('*').get(api.hasConfig);
|
||||||
|
router.route('/config').post(api.configPost);
|
||||||
|
|
||||||
|
router.route('/').get( api.home);
|
||||||
|
router.route('/home').get(api.home);
|
||||||
|
router.route('/search').get(api.search)
|
||||||
|
.post(api.search);
|
||||||
|
router.route('/searchbar').get(api.searchBar);
|
||||||
|
router.route('/recommended').get(api.recommended);
|
||||||
|
router.route('/favorites').get(api.favorites);
|
||||||
|
router.route('/bookmark/:scanlator/:title/:idorLink?').post(api.bookmark(worker));
|
||||||
|
router.route('/manga/:scanlator/:link/:title').get(api.manga);
|
||||||
|
router.route('/chapter/:scanlator/:link/:title/:chapter').get(api.chapter);
|
||||||
|
router.route('/chapternavinfo/:scanlator/:mangaLink/:title/:chapter').get(api.chapterNavInfo);
|
||||||
|
router.route('/dashboard').get(api.dashboard);
|
||||||
|
router.route('/login').get(api.loginPage)
|
||||||
|
.post(api.login);
|
||||||
|
router.route('/logout').get(api.logout);
|
||||||
|
router.route('/chapterRead/:scanlator/:link/:title/:chapter').post(api.chapterRead);
|
||||||
|
|
||||||
|
|
||||||
|
//Undefined Routes v
|
||||||
|
router.use(api.errorPage);
|
||||||
|
return router;
|
||||||
|
};
|
@ -0,0 +1,26 @@
|
|||||||
|
const express = require('express');
|
||||||
|
var app = express();
|
||||||
|
const path = require('path');
|
||||||
|
const http = require('http');
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const morgan = require('morgan')
|
||||||
|
const mongoURI = process.env.DATABASE_URL;
|
||||||
|
const Spawner = require('child_process')
|
||||||
|
|
||||||
|
mongoose.connect(mongoURI);
|
||||||
|
mongoose.connection.on('connected', ()=>{console.log('MongoDB - Connected')})
|
||||||
|
.on('disconnected', ()=>{console.log('MongoDB - Disconnect')})
|
||||||
|
.on('error', (error)=>console.log('Mongoose Error:', error));
|
||||||
|
app.use(express.static(path.join(__dirname, "public")));
|
||||||
|
app.use(morgan('dev'))
|
||||||
|
app.set("view engine", "ejs");
|
||||||
|
app.use(require('cors')())
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({extended:true}));
|
||||||
|
const server = http.createServer(app);
|
||||||
|
server.listen(process.env.PORT, async () =>
|
||||||
|
{
|
||||||
|
const worker = Spawner.fork('./downloader.js', [mongoURI]);
|
||||||
|
app.use('/', require('./routes/routes')(worker));
|
||||||
|
console.log(`Http - Server UP`);
|
||||||
|
});
|
@ -0,0 +1,22 @@
|
|||||||
|
const {SearchByTag, Search, Manga, Modules, cookieParser} = require('./lib');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const mongoose = require('mongoose')
|
||||||
|
const FavoriteModel = require('./models/favorite.js');
|
||||||
|
const mongoURI = 'mongodb://mongoadmin:something@192.168.71.137:27017';
|
||||||
|
|
||||||
|
mongoose.connect(mongoURI);
|
||||||
|
mongoose.connection.on('connected', ()=>{console.log('MongoDB - Connected')})
|
||||||
|
.on('disconnected', ()=>{console.log('MongoDB - Disconnect')})
|
||||||
|
.on('error', (error)=>console.log('Mongoose Error:', error));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(async ()=>
|
||||||
|
{
|
||||||
|
const exists = await new Modules('Asura-scans').titleExists('Absolute Necromancer');
|
||||||
|
const parsed = cookieParser('hash=e941d5c52a2f5aca8237ef40a46749d8619c213683d080f753c5d86f314ae270;coiso=coiso', 'coiso');
|
||||||
|
console.log(parsed)
|
||||||
|
console.log({exists})
|
||||||
|
})()
|
@ -0,0 +1,229 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const mongoURI = 'mongodb://mongoadmin:something@192.168.71.137:27017';
|
||||||
|
|
||||||
|
mongoose.connect(mongoURI);
|
||||||
|
mongoose.connection.on('connected', ()=>{console.log('MongoDB - Connected')})
|
||||||
|
.on('disconnected', ()=>{console.log('MongoDB - Disconnect')})
|
||||||
|
.on('error', (error)=>console.log('Mongoose Error:', error));
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { Stream } = require('stream');
|
||||||
|
const {Manga, Chapter} = require('./lib');
|
||||||
|
const FavModel = require('./models/favorite')
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const Emitter = new EventEmitter();
|
||||||
|
const BaseDir = './public/Saved'
|
||||||
|
|
||||||
|
var Queue = [];
|
||||||
|
var slots = 2;
|
||||||
|
updateQueue();
|
||||||
|
var queueUpdateTimer = setInterval(() => {
|
||||||
|
updateQueue();
|
||||||
|
}, 5*60*1000);
|
||||||
|
|
||||||
|
|
||||||
|
process.on('message', m=>
|
||||||
|
{
|
||||||
|
if(m.action==='add')
|
||||||
|
{
|
||||||
|
clearInterval(queueUpdateTimer);
|
||||||
|
updateQueue();
|
||||||
|
queueUpdateTimer = setInterval(() => {
|
||||||
|
updateQueue();
|
||||||
|
}, 5*60*1000);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(m.action==='remove') return removeFromQueue(m.id)
|
||||||
|
})
|
||||||
|
function removeFromQueue(id)
|
||||||
|
{
|
||||||
|
Queue = Queue.filter(manga => manga._id !== id);
|
||||||
|
}
|
||||||
|
Emitter.on('queueUpdate', ()=>
|
||||||
|
{
|
||||||
|
if(!Queue[0]) return
|
||||||
|
if(!Queue[0].inProgress)
|
||||||
|
{
|
||||||
|
Queue[0].inProgress = true;
|
||||||
|
download(Queue[0])
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
async function updateQueue()
|
||||||
|
{
|
||||||
|
const Scanlators = fs.readdirSync(BaseDir);
|
||||||
|
const Favs = await FavModel.find();
|
||||||
|
let Mangas = [];
|
||||||
|
for(let i =0; i<Scanlators.length; i++)
|
||||||
|
{
|
||||||
|
let mangas = fs.readdirSync(path.join(BaseDir, Scanlators[i]));
|
||||||
|
if(mangas.length < 0) continue
|
||||||
|
for(var j = 0; j<mangas.length; j++)
|
||||||
|
{
|
||||||
|
let chapters = fs.readdirSync(path.join(BaseDir, Scanlators[i], mangas[j]));
|
||||||
|
if(chapters.length==0) continue
|
||||||
|
chapters.sort((a, b) => { return parseInt(a.split('_')[1]) - parseInt(b.split('_')[1]) });
|
||||||
|
let latestDownloaded = parseInt(chapters[chapters.length-1].split('_')[1]);
|
||||||
|
let manga = {scanlator:Scanlators[i], title:mangas[j], latestDownloaded}
|
||||||
|
Mangas.push(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let FavedAndDownloaded = [];
|
||||||
|
let FavedButNotDownloaded = null;
|
||||||
|
for (let i = 0; i < Mangas.length; i++)
|
||||||
|
{
|
||||||
|
FavedAndDownloaded = FavedAndDownloaded.concat(Favs.filter(item =>
|
||||||
|
{
|
||||||
|
return (item.scanlator === Mangas[i].scanlator && item.title === Mangas[i].title);
|
||||||
|
}).map((matchedItem) =>{
|
||||||
|
return ({
|
||||||
|
...matchedItem._doc,
|
||||||
|
latestDownloaded: Mangas[i].latestDownloaded
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
FavedButNotDownloaded = Favs.filter(fav =>
|
||||||
|
{
|
||||||
|
// return !FavedAndDownloaded.includes(fav)
|
||||||
|
return !FavedAndDownloaded.some(fad => fad._id === fav._id);
|
||||||
|
});
|
||||||
|
if(FavedButNotDownloaded)
|
||||||
|
{
|
||||||
|
for(let i = 0; i < FavedButNotDownloaded.length; i++)
|
||||||
|
{
|
||||||
|
(async(i)=>
|
||||||
|
{
|
||||||
|
const {title, link, scanlator, _id} = FavedButNotDownloaded[i];
|
||||||
|
const manga = await new Manga(scanlator, link, title).get();
|
||||||
|
let id = _id.toString();
|
||||||
|
addToQueue(manga, id);
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(FavedAndDownloaded)
|
||||||
|
{
|
||||||
|
for(let i = 0; i<FavedAndDownloaded.length; i++)
|
||||||
|
{
|
||||||
|
(async(i)=>
|
||||||
|
{
|
||||||
|
const {title, link, scanlator, latestDownloaded, _id} = FavedAndDownloaded[i];
|
||||||
|
const manga = await new Manga(scanlator, link, title).get();
|
||||||
|
let id = _id.toString();
|
||||||
|
checkForMissingFiles(manga)
|
||||||
|
if(latestDownloaded<manga.latestChap) addToQueue(manga, id, latestDownloaded);
|
||||||
|
})(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function addToQueue(manga, _id,startFrom=0)
|
||||||
|
{
|
||||||
|
const List = manga.List.filter(item => item.num > startFrom);
|
||||||
|
Queue.push({...manga, _id, List, startFrom})
|
||||||
|
Emitter.emit('queueUpdate', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function checkForMissingFiles(manga)
|
||||||
|
{
|
||||||
|
const {scanlator, title, List} = manga;
|
||||||
|
let auxList = List.sort((a,b)=>{return a.num - b.num})
|
||||||
|
for(var i = 0; i<auxList.length; i++)
|
||||||
|
{
|
||||||
|
const {link} = auxList[i];
|
||||||
|
const destPath = path.join(BaseDir, scanlator, title, 'CH_'+(i+1));
|
||||||
|
const chapterDownloaded = fs.existsSync(destPath);
|
||||||
|
if(!chapterDownloaded) continue;
|
||||||
|
const chapterDir = fs.readdirSync(destPath);
|
||||||
|
const chapter = await new Chapter(scanlator, link, title, i+1).get();
|
||||||
|
let auxChapterDir = chapterDir.map(item=>{return item.split('.')[0]})
|
||||||
|
var auxChapterList = chapter.List.map(item=>{return item.split('/')[item.split('/').length-1].split('.')[0]})
|
||||||
|
const missingImages = auxChapterList.filter(item=>!auxChapterDir.includes(item));
|
||||||
|
if(missingImages.length == 0) continue;
|
||||||
|
for(var j = 0; j<missingImages.length; j++)
|
||||||
|
{
|
||||||
|
const imageLink = chapter.List.filter(item=>item.split('/')[item.split('/').length-1].split('.')[0] == missingImages[j])[0]
|
||||||
|
const destination = `${destPath}/${imageLink.split('/')[(imageLink.split('/').length - 1)]}`
|
||||||
|
const download = await downloadImage(imageLink,destination, 5);
|
||||||
|
console.log('MissingImage:',scanlator, title, 'chapter',i+1, download.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function download(manga)
|
||||||
|
{
|
||||||
|
if(slots <=0) return
|
||||||
|
slots--;
|
||||||
|
const { scanlator, title} = manga;
|
||||||
|
const List = manga.List.sort((a, b) => { return a.num - b.num });
|
||||||
|
for (let i = 0; i < List.length; i++)
|
||||||
|
{
|
||||||
|
const ch = List[i];
|
||||||
|
if (isNaN(ch.num)) continue;
|
||||||
|
const { num, link } = ch;
|
||||||
|
const chapter = await new Chapter(scanlator, link, title, num).get();
|
||||||
|
// console.log(chapter)
|
||||||
|
if (!chapter?.List?.length) continue;
|
||||||
|
await downloadChapter(chapter);
|
||||||
|
}
|
||||||
|
Queue.shift();
|
||||||
|
slots++;
|
||||||
|
Emitter.emit('queueUpdate', '');
|
||||||
|
}
|
||||||
|
async function downloadChapter(chapter)
|
||||||
|
{
|
||||||
|
const {List, scanlator, title, chNum } = chapter;
|
||||||
|
const images = List;
|
||||||
|
for (let i = 0; i < images.length; i++) {
|
||||||
|
const img = images[i];
|
||||||
|
const destination = `./public/Saved/${scanlator}/${title}/CH_${chNum}/${img.split('/')[(img.split('/').length - 1)]}`;
|
||||||
|
try {
|
||||||
|
const downloaded = await downloadImage(img, destination);
|
||||||
|
console.log(`Downloaded: Scanlator:${scanlator}; Manga: ${title} Chapter:${chNum}; IMG:${img.split('/')[(img.split('/').length - 1)]} ${downloaded.status}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error downloading ${img}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {chNum}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Link} url
|
||||||
|
* @param {Path} destPath
|
||||||
|
* @param {Number} maxRetries
|
||||||
|
* @returns {Download}
|
||||||
|
* @typedef {String} Link
|
||||||
|
* @typedef {Object} Download
|
||||||
|
* @property {String} status
|
||||||
|
* @property {Path} file
|
||||||
|
*/
|
||||||
|
async function downloadImage(url, destPath, maxRetries = 3)
|
||||||
|
{
|
||||||
|
let retries = 0;
|
||||||
|
while (retries < maxRetries) {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const response = await fetch(url);
|
||||||
|
const dir = path.dirname(destPath);
|
||||||
|
if (!response.ok) throw new Error(`Failed to download image. Status Code: ${response.status}`);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
const fileStream = fs.createWriteStream(destPath);
|
||||||
|
await new Promise((resolve, reject) =>
|
||||||
|
{
|
||||||
|
const stream = Stream.Readable.fromWeb(response.body).pipe(fileStream);
|
||||||
|
stream.on('error', reject);
|
||||||
|
fileStream.on('finish', () => {
|
||||||
|
fileStream.close();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return {status:'Downloaded', file:destPath}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error downloading image: ${destPath}, Retry ${retries + 1}/${maxRetries}`);//, error);
|
||||||
|
retries++;
|
||||||
|
if(retries == maxRetries)
|
||||||
|
return {status:'Failed', file:destPath}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
<span class="material-symbols-outlined bookmark" hx-post="/bookmark/<%=scanlator%>/<%=title%>/<%= link %>" hx-trigger="click" hx-target="this" hx-swap="outerHTML">
|
||||||
|
bookmark
|
||||||
|
</span>
|
||||||
|
|
@ -0,0 +1,3 @@
|
|||||||
|
<span class="material-symbols-outlined bookmark fill " hx-post="/bookmark/<%=scanlator%>/<%=title%>/<%= id %> " hx-trigger="click" hx-target="this"hx-swap="outerHTML">
|
||||||
|
bookmark
|
||||||
|
</span>
|
@ -0,0 +1,10 @@
|
|||||||
|
<div class="chapterButton">
|
||||||
|
<a hx-get="/chapter/<%=scanlator %>/<%=encodeURIComponent(chapter.link)%> /<%= title %>/<%= chapter.num %>" hx-trigger="click" hx-target="#container" hx-indicator=".progress">
|
||||||
|
<h4 class="chapterNum">
|
||||||
|
Chapter <%= chapter.num %>
|
||||||
|
</h4>
|
||||||
|
<h5 class="date">
|
||||||
|
<%= chapter.date %>
|
||||||
|
</h5>
|
||||||
|
</a>
|
||||||
|
</div>
|
@ -0,0 +1,15 @@
|
|||||||
|
<div class="chapterNav">
|
||||||
|
<% if(data.chapterNum!=1){%>
|
||||||
|
<div class="chapterNavButton" hx-get="/chapter/<%=data.scanlator %>/<%= encodeURIComponent(data.prevChapterLink) %>/<%= data.title %>/<%= data.chapterNum-1 %>" hx-trigger="click" hx-target="#container">
|
||||||
|
<h3 class="prev">Prev</h3>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
<h1 class="chapterNavChapterLabel"
|
||||||
|
hx-get="/manga/<%=data.scanlator %>/<%= encodeURIComponent(data.mangaLink) %>/<%= data.title %>" hx-trigger="click" hx-target="#container"
|
||||||
|
> <%= data.title %> - Chapter: <%= data.chapterNum %> </h1>
|
||||||
|
<% if(data.chapterNum<data.latestChap){%>
|
||||||
|
<div class="chapterNavButton" hx-get="/chapter/<%=data.scanlator %>/<%= encodeURIComponent(data.nextChapterLink) %>/<%= data.title %>/<%= data.chapterNum+1 %>" hx-trigger="click" hx-target="#container">
|
||||||
|
<h3 class="prev">Next</h3>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
@ -0,0 +1,137 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-type" content="text/html;charset=UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link rel="icon" type="image/ico" href="images/favicon.ico"/>
|
||||||
|
<title>Manga Reader</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/home.css" id="pagesheet"/>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
||||||
|
<script type="module" src="/js/home.js"></script>
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="progress" style="height: 3px; background-color: white;">
|
||||||
|
<div class="indeterminate" style="background-color: red;"></div>
|
||||||
|
</div>
|
||||||
|
<div id="configWrapper">
|
||||||
|
<form class="configForm" id="configForm" action="/config" method="post" >
|
||||||
|
<section class="usernameSection">
|
||||||
|
<!-- <div class="marker"></div> -->
|
||||||
|
<label for="username" class="username">Username:</label>
|
||||||
|
<input type="text" name="username" id="username" class="formInput" required>
|
||||||
|
</section>
|
||||||
|
<section class="passwordSection">
|
||||||
|
<!-- <div class="marker"></div> -->
|
||||||
|
<label for="password" class="password">Password:</label>
|
||||||
|
<input type="password" name="password" id="password"class="formInput" required>
|
||||||
|
<label for="password2" class="password">Confirm password:</label>
|
||||||
|
<input type="password" name="password2"id="password2" class="formInput" required>
|
||||||
|
</section>
|
||||||
|
<section class="folderPathSection">
|
||||||
|
<!-- <div class="marker"></div> -->
|
||||||
|
<label for="saveFolder" class="saveFolder">Save folder Path: </label>
|
||||||
|
<input type="text" name="saveFolder" id="saveFolder" class="formInput" list="dirs" placeholder="Can be left blank if default is fine">
|
||||||
|
<datalist id="dirs">
|
||||||
|
<% data.forEach(dir=>{ %>
|
||||||
|
<option value="<%= dir %>">
|
||||||
|
<%}) %>
|
||||||
|
</datalist>
|
||||||
|
</section>
|
||||||
|
<section class="buttonSection">
|
||||||
|
<button id="configSubmitBt" type="submit">Configure</button>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
const regex = /^[a-zA-Z]+$/;
|
||||||
|
const Errors = {
|
||||||
|
Mismatch:'Passwords don\'t match.',
|
||||||
|
Short:'Password is too short. (min 10 characters)',
|
||||||
|
Arguments:'Password must have special characters.',
|
||||||
|
}
|
||||||
|
function handler(event)
|
||||||
|
{
|
||||||
|
let passwordInput = document.getElementById('password');
|
||||||
|
let password2Input = document.getElementById('password2');
|
||||||
|
let password = passwordInput.value;
|
||||||
|
let password2 = password2Input.value;
|
||||||
|
let userInput = document.getElementById('username');
|
||||||
|
let user = userInput.value;
|
||||||
|
resetValidity([passwordInput, password2Input, userInput])
|
||||||
|
console.log('Chec')
|
||||||
|
if(user.length < 4) return userInput.setCustomValidity(Errors.Short);
|
||||||
|
if(password.length < 10) return passwordInput.setCustomValidity(Errors.Short);
|
||||||
|
if(password!=password2) return password2Input.setCustomValidity(Errors.Mismatch);
|
||||||
|
if(regex.test(password)) return passwordInput.setCustomValidity(Errors.Arguments);
|
||||||
|
}
|
||||||
|
function resetValidity(Elements)
|
||||||
|
{
|
||||||
|
for(var element of Elements)
|
||||||
|
{
|
||||||
|
element.setCustomValidity('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('username').addEventListener('keyup', handler);
|
||||||
|
document.getElementById('password').addEventListener('keyup', handler);
|
||||||
|
document.getElementById('password2').addEventListener('keyup', handler);
|
||||||
|
document.getElementById('saveFolder').addEventListener('keyup', handler);
|
||||||
|
input.onfocus = function () {
|
||||||
|
browsers.style.display = 'block';
|
||||||
|
input.style.borderRadius = "5px 5px 0 0";
|
||||||
|
};
|
||||||
|
for (let option of browsers.options) {
|
||||||
|
option.onclick = function () {
|
||||||
|
input.value = option.value;
|
||||||
|
browsers.style.display = 'none';
|
||||||
|
input.style.borderRadius = "5px";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
input.oninput = function() {
|
||||||
|
currentFocus = -1;
|
||||||
|
var text = input.value.toUpperCase();
|
||||||
|
for (let option of browsers.options) {
|
||||||
|
if(option.value.toUpperCase().indexOf(text) > -1){
|
||||||
|
option.style.display = "block";
|
||||||
|
}else{
|
||||||
|
option.style.display = "none";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var currentFocus = -1;
|
||||||
|
input.onkeydown = function(e) {
|
||||||
|
if(e.keyCode == 40){
|
||||||
|
currentFocus++
|
||||||
|
addActive(browsers.options);
|
||||||
|
}
|
||||||
|
else if(e.keyCode == 38){
|
||||||
|
currentFocus--
|
||||||
|
addActive(browsers.options);
|
||||||
|
}
|
||||||
|
else if(e.keyCode == 13){
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentFocus > -1) {
|
||||||
|
/*and simulate a click on the "active" item:*/
|
||||||
|
if (browsers.options) browsers.options[currentFocus].click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addActive(x) {
|
||||||
|
if (!x) return false;
|
||||||
|
removeActive(x);
|
||||||
|
if (currentFocus >= x.length) currentFocus = 0;
|
||||||
|
if (currentFocus < 0) currentFocus = (x.length - 1);
|
||||||
|
x[currentFocus].classList.add("active");
|
||||||
|
}
|
||||||
|
function removeActive(x) {
|
||||||
|
for (var i = 0; i < x.length; i++) {
|
||||||
|
x[i].classList.remove("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</html>
|
@ -0,0 +1,169 @@
|
|||||||
|
|
||||||
|
<div class="chapterOverview" id="chapterOverview">
|
||||||
|
<div class="flexBox" id="flexBox">
|
||||||
|
<% for(var img of data.List){ %>
|
||||||
|
<img class="overview" src=<%= img.replaceAll(' ','%20') %> />
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<div class="expand" onclick="toggle()">
|
||||||
|
<span class="material-symbols-outlined" id="expandChevron">chevron_left</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wrapperReaderBox">
|
||||||
|
<div class="spacer" id="spacer"></div>
|
||||||
|
<div class="readerDisplay">
|
||||||
|
|
||||||
|
<div class="chapterNav"
|
||||||
|
hx-get="/chapternavinfo/<%= data.scanlator %>/<%= encodeURIComponent(data.mangaLink)%>/<%= data.title %>/<%= data.chapterNum %>"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-indicator=".progress"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button class="pullBackUp" onclick="pullUp()"> <span class="material-symbols-outlined chevronUp">expand_less</span></button>
|
||||||
|
<div id="display">
|
||||||
|
<% for(var img of data.List){ %>
|
||||||
|
<img src=<%= img.replaceAll(' ','%20') %> />
|
||||||
|
<% } %>
|
||||||
|
<div id="last-image"
|
||||||
|
hx-post="/chapterRead/<%= data.scanlator %>/<%= encodeURIComponent(data.mangaLink)%>/<%= data.title %>/<%= data.chapterNum %>"
|
||||||
|
hx-swap="none"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="chapterNav "
|
||||||
|
hx-get="/chapternavinfo/<%= data.scanlator %>/<%= encodeURIComponent(data.mangaLink)%>/<%= data.title %>/<%= data.chapterNum %>"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-indicator=".progress"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
clearVariables();
|
||||||
|
function pullUp()
|
||||||
|
{
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', function() {
|
||||||
|
var elements = document.querySelectorAll('.your-element-class'); // Replace '.your-element-class' with the class of the elements you want to check
|
||||||
|
var windowHeight = window.innerHeight;
|
||||||
|
|
||||||
|
elements.forEach(function(element) {
|
||||||
|
var bounding = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (
|
||||||
|
bounding.top >= 0 &&
|
||||||
|
bounding.bottom <= windowHeight
|
||||||
|
) {
|
||||||
|
// Element is in view
|
||||||
|
console.log(element.textContent + ' is in view.');
|
||||||
|
} else {
|
||||||
|
// Element is not in view
|
||||||
|
console.log(element.textContent + ' is not in view.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
function toggle()
|
||||||
|
{
|
||||||
|
let flexBox = document.getElementById('flexBox');
|
||||||
|
if( flexBox.style.display!='none') return closePreview();
|
||||||
|
return openPreview();
|
||||||
|
|
||||||
|
}
|
||||||
|
function openPreview()
|
||||||
|
{
|
||||||
|
let flexBox = document.getElementById('flexBox');
|
||||||
|
let chapterOverview = document.getElementById('chapterOverview');
|
||||||
|
let chevron = document.getElementById('expandChevron');
|
||||||
|
let spacer = document.getElementById('spacer');
|
||||||
|
flexBox.style.display='flex';
|
||||||
|
chevron.innerText='chevron_left';
|
||||||
|
chapterOverview.style.width = '25vw';
|
||||||
|
spacer.style.width = '25%';
|
||||||
|
setCookie("preview", "preview", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePreview()
|
||||||
|
{
|
||||||
|
let flexBox = document.getElementById('flexBox');
|
||||||
|
let chapterOverview = document.getElementById('chapterOverview');
|
||||||
|
let chevron = document.getElementById('expandChevron');
|
||||||
|
let spacer = document.getElementById('spacer');
|
||||||
|
flexBox.style.display = 'none';
|
||||||
|
chevron.innerText='chevron_right';
|
||||||
|
chapterOverview.style.width = '1vw';
|
||||||
|
spacer.style.width = '0%';
|
||||||
|
setCookie("preview", "dontpreview", 1)
|
||||||
|
}
|
||||||
|
function setCookie(name, value, days)
|
||||||
|
{
|
||||||
|
var expires = "";
|
||||||
|
if (days) {
|
||||||
|
var date = new Date();
|
||||||
|
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||||
|
expires = "; expires=" + date.toUTCString();
|
||||||
|
}
|
||||||
|
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||||
|
}
|
||||||
|
function getCookie(name)
|
||||||
|
{
|
||||||
|
if(!document.cookie) return '';
|
||||||
|
return document.cookie.split(name+'=')[1].split(';')[0];
|
||||||
|
}
|
||||||
|
function clearVariables()
|
||||||
|
{
|
||||||
|
[ 'lastImage',
|
||||||
|
'observer',
|
||||||
|
'flexBox',
|
||||||
|
'chapterOverview',
|
||||||
|
'chevron',
|
||||||
|
'spacer',
|
||||||
|
'preview',
|
||||||
|
].forEach((variable) =>
|
||||||
|
{
|
||||||
|
if (window.hasOwnProperty(variable))
|
||||||
|
{
|
||||||
|
delete window[variable];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function hasFinished()
|
||||||
|
{
|
||||||
|
const lastImage = document.getElementById('last-image');
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(entries =>
|
||||||
|
{
|
||||||
|
if (entries[0].isIntersecting)
|
||||||
|
{
|
||||||
|
lastImage.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(lastImage);
|
||||||
|
}
|
||||||
|
function checkPreview()
|
||||||
|
{
|
||||||
|
let preview = getCookie('preview');
|
||||||
|
console.log(preview, preview == 'preview')
|
||||||
|
if(preview=='preview') return openPreview();
|
||||||
|
return closePreview();
|
||||||
|
}
|
||||||
|
function imageInView()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
hasFinished();
|
||||||
|
imageInView();
|
||||||
|
checkPreview();
|
||||||
|
} catch (error) {
|
||||||
|
clearVariables();
|
||||||
|
hasFinished();
|
||||||
|
imageInView();
|
||||||
|
checkPreview();
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,17 @@
|
|||||||
|
<% if(!isEmpty){%>
|
||||||
|
<% mangas.forEach(scanlator=>{ %>
|
||||||
|
<div class="scanlator-container">
|
||||||
|
<h2><%= scanlator.scanlator %></h2>
|
||||||
|
<br>
|
||||||
|
<div class="searchCarousel" >
|
||||||
|
<%- include('searchCarousel.ejs', {Results:scanlator.Results, scan:scanlator} )%>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<% } %>
|
||||||
|
<% if(isEmpty){ %>
|
||||||
|
<h2 class="NoFavs">
|
||||||
|
You haven't favorited any manga/manhwa!
|
||||||
|
</h2>
|
||||||
|
<% } %>
|
||||||
|
|
@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-type" content="text/html;charset=UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link rel="icon" type="image/ico" href="images/favicon.ico"/>
|
||||||
|
<title>Manga Reader</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/home.css" id="pagesheet"/>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
||||||
|
<script type="module" src="/js/home.js"></script>
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="progress" style="height: 3px; background-color: white;">
|
||||||
|
<div class="indeterminate" style="background-color: red;"></div>
|
||||||
|
</div>
|
||||||
|
<div id="wrapper">
|
||||||
|
<%- include('navBar.ejs', {data}) %>
|
||||||
|
<div class="container" id="container" >
|
||||||
|
<h2 id="RecommendTitle">Recommended</h2>
|
||||||
|
<div class="recommended" hx-get="/recommended" hx-trigger="load" hx-indicator=".progress"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,21 @@
|
|||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<div class="formWrapper">
|
||||||
|
<form class="loginForm" id="loginForm" action="/login" method="post" >
|
||||||
|
<div class="loginFormFlexBox">
|
||||||
|
<section class="usernameSection">
|
||||||
|
<!-- <div class="marker"></div> -->
|
||||||
|
<label for="username" class="username">Username:</label>
|
||||||
|
<input type="text" name="username" id="username" class="formInput" required>
|
||||||
|
</section>
|
||||||
|
<section class="passwordSection">
|
||||||
|
<!-- <div class="marker"></div> -->
|
||||||
|
<label for="password" class="password">Password:</label>
|
||||||
|
<input type="password" name="password" id="password"class="formInput" required>
|
||||||
|
</section>
|
||||||
|
<section class="buttonSection">
|
||||||
|
<button id="loginSubmitBt" type="submit">Login</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
@ -0,0 +1,26 @@
|
|||||||
|
<div class="infoCard">
|
||||||
|
<img src="<%=data.img%> " alt="<%= data.title %> Cover Image">
|
||||||
|
<div class="infoText">
|
||||||
|
<a href="<%=data.link%>" target="_blank"></a>
|
||||||
|
<% if(data.Status=='Ongoing'){%>
|
||||||
|
<h2 class="title">🟢<%=data.title %> </h2>
|
||||||
|
<% }else{ %>
|
||||||
|
<h2 class="title">🔴<%=data.title %> </h2>
|
||||||
|
<% } %>
|
||||||
|
</a>
|
||||||
|
<p class="description"> <%= data.description %> </p>
|
||||||
|
<div class="tags">
|
||||||
|
<% data.tags.forEach(tag=>{ %>
|
||||||
|
<div class="tag">
|
||||||
|
<%=tag %>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="list">
|
||||||
|
<% data.List.forEach(chapter=>{%>
|
||||||
|
<%- include('chapterButton.ejs', {chapter, scanlator:data.scanlator, title:data.title}) %>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
<div class="nav">
|
||||||
|
<div class="button-container" id="button-container" >
|
||||||
|
<div class="bt first"
|
||||||
|
hx-get="/home"
|
||||||
|
hx-select="#container"
|
||||||
|
hx-indicator=".progress"
|
||||||
|
hx-on:click="resetSearch()"
|
||||||
|
hx-target="#container"
|
||||||
|
>
|
||||||
|
<h1 class="bt-inner">Home</h1>
|
||||||
|
</div>
|
||||||
|
<div class="bt"
|
||||||
|
hx-get="/favorites"
|
||||||
|
hx-target="#container"
|
||||||
|
hx-on:click="resetSearch()"
|
||||||
|
hx-indicator=".progress"
|
||||||
|
>
|
||||||
|
<h1 class="bt-inner">Collection</h1>
|
||||||
|
</div>
|
||||||
|
<%- include('searchBar.ejs') %>
|
||||||
|
</div>
|
||||||
|
<% if(!data){%>
|
||||||
|
<button class="login"
|
||||||
|
hx-get="/login"
|
||||||
|
hx-indicator=".progress"
|
||||||
|
hx-target="#container"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
<% } else {%>
|
||||||
|
<button class="login burgerMenu"
|
||||||
|
hx-get="/dashboard"
|
||||||
|
hx-indicator=".progress"
|
||||||
|
hx-target="#container"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined">
|
||||||
|
menu
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function resetSearch()
|
||||||
|
{
|
||||||
|
let search = document.getElementById('search');
|
||||||
|
search.value = '';
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,18 @@
|
|||||||
|
<div class="searchBarWrapper" id="searchBarWrapper">
|
||||||
|
<input name="searchstring"
|
||||||
|
hx-post="/search"
|
||||||
|
hx-trigger="keyup[keyCode==13]"
|
||||||
|
hx-indicator=".progress"
|
||||||
|
hx-target="#container"
|
||||||
|
id="search"
|
||||||
|
placeholder="Search">
|
||||||
|
</input>
|
||||||
|
<span class="material-symbols-outlined searchIcon" onclick="triggerInputClick()">search</span>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function triggerInputClick() {
|
||||||
|
var inputElement = document.getElementById('search');
|
||||||
|
var enterEvent = new KeyboardEvent('keyup', {keyCode: 13});
|
||||||
|
inputElement.dispatchEvent(enterEvent);
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,20 @@
|
|||||||
|
<article class="card">
|
||||||
|
<% let scanlatorID = scan.scanlator.replace('-', '') %>
|
||||||
|
<div class="card__img img_<%=scanlatorID %>" style="background-image: url(<%=result.img%>)">
|
||||||
|
<% if(result.favorite){%>
|
||||||
|
<%-include('bookmarked.ejs', {scanlator, title:result.title, link:encodeURIComponent(result.link), id:result.favorite}) %>
|
||||||
|
<% } else { %>
|
||||||
|
<%-include('bookmark.ejs', {scanlator, title:result.title, link:encodeURIComponent(result.link)}) %>
|
||||||
|
<% } %>
|
||||||
|
<%- include('status.ejs', {status:result.Status}) %>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="card__info"
|
||||||
|
hx-get="/manga/<%= scanlator %>/<%=result.link?encodeURIComponent(result.link):'nolink'%>/<%= result.title %>"
|
||||||
|
hx-target="#container"
|
||||||
|
hx-trigger="click"
|
||||||
|
>
|
||||||
|
<h3 class="card__title"><%= result.title %> </h3>
|
||||||
|
<p> Latest Chapter: <%= result.latestChap %> </p>
|
||||||
|
</div>
|
||||||
|
</article>
|
@ -0,0 +1,112 @@
|
|||||||
|
<div class="carousel">
|
||||||
|
<% let scanlatorID = scan.scanlator.replace('-', '') %>
|
||||||
|
<div class="cards-container cards-container_<%=scanlatorID%>">
|
||||||
|
<% scan.Results.forEach(result=>{%>
|
||||||
|
<%- include('searchCards.ejs', {result, scanlator:scan.scanlator}) %>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
<div class="buttonContainer" id="buttonContainer<%= scanlatorID %>">
|
||||||
|
<button class="prev-btn" onclick="handlePrev_<%= scanlatorID %>()"><span class="material-symbols-outlined chevron">
|
||||||
|
chevron_left
|
||||||
|
</span></button>
|
||||||
|
<button class="next-btn" onclick="handleNext_<%= scanlatorID %>()"><span class="material-symbols-outlined chevron">
|
||||||
|
chevron_right
|
||||||
|
</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
clearVariables_<%= scanlatorID %>();
|
||||||
|
needsChevrons_<%= scanlatorID %>();
|
||||||
|
|
||||||
|
function clearVariables_<%= scanlatorID %>()
|
||||||
|
{
|
||||||
|
['cardsContainer_<%= scanlatorID %>',
|
||||||
|
'cardsAmount_<%= scanlatorID %>',
|
||||||
|
'cardWidth_<%= scanlatorID %>',
|
||||||
|
'currentPosition_<%= scanlatorID %>',
|
||||||
|
'maxPosition_<%= scanlatorID %>',
|
||||||
|
'imageAmount_<%= scanlatorID %>',
|
||||||
|
'imageOffSetWith_<%= scanlatorID %>',
|
||||||
|
'carouselWidth_<%= scanlatorID %>',
|
||||||
|
'gap_<%= scanlatorID %>',
|
||||||
|
'cards_<%= scanlatorID %>',
|
||||||
|
'lastCard_<%= scanlatorID %>',
|
||||||
|
'lastCardBoundingRect_<%= scanlatorID %>',
|
||||||
|
'nextButtonBoudingRect_<%= scanlatorID %>'].forEach((variable) =>
|
||||||
|
{
|
||||||
|
if (window.hasOwnProperty(variable))
|
||||||
|
{
|
||||||
|
delete window[variable];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
// Initializing variables
|
||||||
|
try {
|
||||||
|
var cardAmount_<%= scanlatorID %> = document.querySelectorAll('.cards-container_<%=scanlatorID %> .card').length - 3;
|
||||||
|
var cardWidth_<%= scanlatorID %> = document.querySelector('.cards-container_<%=scanlatorID %> .card').offsetWidth;
|
||||||
|
var currentPosition_<%= scanlatorID %> = 0;
|
||||||
|
var maxPosition_<%= scanlatorID %> = - cardAmount_<%= scanlatorID %>;
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
clearVariables_<%= scanlatorID %>();
|
||||||
|
var cardAmount_<%= scanlatorID %> = document.querySelectorAll('.cards-container_<%=scanlatorID %> .card').length - 3;
|
||||||
|
var cardWidth_<%= scanlatorID %> = document.querySelector('.cards-container_<%=scanlatorID %> .card').offsetWidth;
|
||||||
|
var currentPosition_<%= scanlatorID %> = 0;
|
||||||
|
var maxPosition_<%= scanlatorID %> = - cardAmount_<%= scanlatorID %>;
|
||||||
|
|
||||||
|
}
|
||||||
|
function needsChevrons_<%= scanlatorID %>()
|
||||||
|
{
|
||||||
|
let cardsContainer_<%= scanlatorID %> = document.querySelectorAll('.cards-container_<%= scanlatorID %>')[0]
|
||||||
|
let gap_<%= scanlatorID %> = parseFloat(window.getComputedStyle(cardsContainer_<%= scanlatorID %>).gap);
|
||||||
|
let imageAmount_<%= scanlatorID %> = document.querySelectorAll('.img_<%= scanlatorID %>').length;
|
||||||
|
let imageOffSetWith_<%= scanlatorID %> = document.querySelectorAll('.img_<%= scanlatorID %>')[0].offsetWidth+gap_<%= scanlatorID %>
|
||||||
|
let carouselWidth_<%= scanlatorID %> = document.getElementById('buttonContainer<%= scanlatorID %>').offsetWidth;
|
||||||
|
// console.log('Chevron check', carouselWidth_<%= scanlatorID %>,(imageAmount_<%= scanlatorID %> * imageOffSetWith_<%= scanlatorID %>),(imageAmount_<%= scanlatorID %> * imageOffSetWith_<%= scanlatorID %>) < carouselWidth_<%= scanlatorID %>)
|
||||||
|
if ((imageAmount_<%= scanlatorID %> * imageOffSetWith_<%= scanlatorID %>) < carouselWidth_<%= scanlatorID %>)
|
||||||
|
{
|
||||||
|
return document.getElementById('buttonContainer<%= scanlatorID %>').classList.add('hidden');
|
||||||
|
}
|
||||||
|
document.getElementById('buttonContainer<%= scanlatorID %>').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hiding button container if all cards fit in view
|
||||||
|
window.addEventListener('resize', (event) =>
|
||||||
|
{
|
||||||
|
needsChevrons_<%= scanlatorID %>();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Function to handle previous button click
|
||||||
|
function handlePrev_<%= scanlatorID %>()
|
||||||
|
{
|
||||||
|
let cardsContainer_<%= scanlatorID %> = document.querySelector('.cards-container_<%=scanlatorID %>');
|
||||||
|
if (currentPosition_<%= scanlatorID %> == 0) return;
|
||||||
|
currentPosition_<%= scanlatorID %>++;
|
||||||
|
cardsContainer_<%= scanlatorID %>.style.transform = `translateX(${currentPosition_<%= scanlatorID %>*cardWidth_<%= scanlatorID %>}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to handle next button click
|
||||||
|
function handleNext_<%= scanlatorID %>()
|
||||||
|
{
|
||||||
|
let cardsContainer_<%= scanlatorID %> = document.querySelector('.cards-container_<%= scanlatorID %>');
|
||||||
|
let cards_<%= scanlatorID %> = document.querySelectorAll('.cards-container_<%= scanlatorID %> .card')
|
||||||
|
let lastCard_<%= scanlatorID %> = cards_<%= scanlatorID %>[cards_<%= scanlatorID %>.length-1];
|
||||||
|
let lastCardBoundingRect_<%= scanlatorID %> = lastCard_<%= scanlatorID %>.getBoundingClientRect();
|
||||||
|
let nextButtonBoudingRect_<%= scanlatorID %> = document.querySelector('#buttonContainer<%= scanlatorID %> .next-btn').getBoundingClientRect();
|
||||||
|
if((lastCardBoundingRect_<%= scanlatorID %>.x+lastCardBoundingRect_<%= scanlatorID %>.width)<nextButtonBoudingRect_<%= scanlatorID %>.x) return;
|
||||||
|
if (currentPosition_<%= scanlatorID %> == maxPosition_<%= scanlatorID %>) return;
|
||||||
|
currentPosition_<%= scanlatorID %>--;
|
||||||
|
cardsContainer_<%= scanlatorID %>.style.transform = `translateX(${currentPosition_<%= scanlatorID %>*cardWidth_<%= scanlatorID %>}px)`;
|
||||||
|
cards_<%= scanlatorID %> = document.querySelectorAll('.cards-container_<%= scanlatorID %> .card')
|
||||||
|
lastCard_<%= scanlatorID %> = cards_<%= scanlatorID %>[cards_<%= scanlatorID %>.length-1];
|
||||||
|
lastCardBoundingRect_<%= scanlatorID %> = lastCard_<%= scanlatorID %>.getBoundingClientRect();
|
||||||
|
nextButtonBoudingRect_<%= scanlatorID %> = document.querySelector('#buttonContainer<%= scanlatorID %> .next-btn').getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
@ -0,0 +1,9 @@
|
|||||||
|
<% data.forEach(scan=>{ %>
|
||||||
|
<div class="scanlator-container">
|
||||||
|
<h2><%= scan.scanlator %></h2>
|
||||||
|
<br>
|
||||||
|
<div class="searchCarousel">
|
||||||
|
<%- include('searchCarousel.ejs', {scan} )%>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
@ -0,0 +1,5 @@
|
|||||||
|
<% if(status && status != 'Ongoing'){ %>
|
||||||
|
<span class="status <%=status%>">
|
||||||
|
<%= status %>
|
||||||
|
</span>
|
||||||
|
<% } %>
|
Loading…
Reference in new issue