master
masterhc 1 year ago
commit 8ccc1db550

@ -0,0 +1,2 @@
DATABASE_URL=mongodb://mongoadmin:something@192.168.71.137:27017
PORT=5010

2
.gitignore vendored

@ -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}
}
}
}

478
lib.js

@ -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(/&nbsp;|&#(\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"]
}

2154
package-lock.json generated

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…
Cancel
Save