added backend to cache manga updates requests

This commit is contained in:
wea_ondara
2023-11-20 17:01:37 +01:00
parent 34b0135da7
commit 29e1d2e499
19 changed files with 699 additions and 20 deletions

2
.gitignore vendored
View File

@@ -28,3 +28,5 @@ frontend/.vscode/*
*.sw? *.sw?
package-lock.json package-lock.json
dev
/backend/cache/

4
backend/config.json Normal file
View File

@@ -0,0 +1,4 @@
{
"host": "localhost",
"port": 5000
}

29
backend/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "mangastatus-backend",
"version": "1.0.0",
"private": true,
"author": "wea_ondara",
"license": "ISC",
"scripts": {
"dev": "run-p dev:1 dev:2",
"dev:1": "tsc --watch --preserveWatchOutput -p tsconfig.dev.json",
"dev:2": "wait-on dev/main.js && nodemon -q --watch dev/ dev/main.js",
"build": "run-s type-check build-only",
"build-only": "tsc",
"type-check": "tsc --noEmit --composite false"
},
"dependencies": {
"compression": "^1.7.4",
"express": "^4.18.2",
"node-schedule": "^2.1.1"
},
"devDependencies": {
"@types/compression": "^1.7.5",
"@types/express": "^4.17.21",
"@types/node-schedule": "^2.1.3",
"nodemon": "^3.0.1",
"npm-run-all": "^4.1.5",
"typescript": "^5.2.2",
"wait-on": "^7.2.0"
}
}

98
backend/src/cache/MangaUpdatesCache.ts vendored Normal file
View File

@@ -0,0 +1,98 @@
import * as fs from 'fs';
type CacheEntry = { data: string, lastUpdateMs: number }
export class MangaUpdatesCache {
private readonly FILE_CACHE_DIR = 'cache';
private readonly FILE_SEARCH_BY_TITLE = this.FILE_CACHE_DIR + '/searchByTitle.json';
private readonly FILE_SERIES_BY_ID = this.FILE_CACHE_DIR + '/seriesById.json';
private readonly FILE_SERIES_GROUPS_BY_ID = this.FILE_CACHE_DIR + '/seriesGroupsById.json';
private readonly MAX_CACHE_AGE_SEARCH_BY_TITLE = 7 * 24 * 60 * 60 * 1000;
private readonly MAX_CACHE_AGE_SERIES_BY_ID = 30 * 24 * 60 * 60 * 1000;
private readonly MAX_CACHE_AGE_SERIES_GROUPS_BY_ID = 1 * 24 * 60 * 60 * 1000;
private _searchByTitle = new Map<string, CacheEntry>();
private _seriesById = new Map<string, CacheEntry>();
private _seriesGroupsById = new Map<string, CacheEntry>();
constructor() {
this.load();
}
private load(): void {
try {
fs.mkdirSync(this.FILE_CACHE_DIR);
} catch (_) {
}
this._searchByTitle = this.loadJson(this.FILE_SEARCH_BY_TITLE);
this._seriesById = this.loadJson(this.FILE_SERIES_BY_ID);
this._seriesGroupsById = this.loadJson(this.FILE_SERIES_GROUPS_BY_ID);
}
private loadJson(file: string): Map<string, CacheEntry> {
try {
const data = fs.readFileSync(file, {encoding: 'utf8', flag: 'r'});
const json = JSON.parse(data);
return new Map<string, CacheEntry>(Object.entries(json));
} catch (err) {
console.error('Failed to load cache from ' + file + ':' + (err as Error).message);
return new Map();
}
}
private saveJson(file: string, map: Map<string, CacheEntry>): void {
try {
const data = JSON.stringify(Object.fromEntries(map.entries()));
fs.writeFileSync(file, data, {encoding: 'utf8', flag: 'w'});
} catch (err) {
console.error(err);
}
}
getSearchByTitle(title: string): string | undefined {
const entry = this._searchByTitle.get(title);
return !entry || entry.lastUpdateMs + this.MAX_CACHE_AGE_SEARCH_BY_TITLE < Date.now() ? undefined : entry.data;
}
getSeriesById(id: string): string | undefined {
const entry = this._seriesById.get(id);
return !entry || entry.lastUpdateMs + this.MAX_CACHE_AGE_SERIES_BY_ID < Date.now() ? undefined : entry.data;
}
getSeriesGroupsById(id: string): string | undefined {
const entry = this._seriesGroupsById.get(id);
return !entry || entry.lastUpdateMs + this.MAX_CACHE_AGE_SERIES_GROUPS_BY_ID < Date.now() ? undefined : entry.data;
}
putSearchByTitle(title: string, value: string): void {
this._searchByTitle.set(title, {data: value, lastUpdateMs: Date.now()});
this.saveJson(this.FILE_SEARCH_BY_TITLE, this._searchByTitle);
}
putSeriesById(id: string, value: string): void {
this._seriesById.set(id, {data: value, lastUpdateMs: Date.now()});
this.saveJson(this.FILE_SERIES_BY_ID, this._seriesById);
}
putSeriesGroupsById(id: string, value: string): void {
this._seriesGroupsById.set(id, {data: value, lastUpdateMs: Date.now()});
this.saveJson(this.FILE_SERIES_GROUPS_BY_ID, this._seriesGroupsById);
}
getOutOfDateSearch(): string[] {
return Array.from(this._searchByTitle.entries())
.filter(([title, entry]) => entry.lastUpdateMs + this.MAX_CACHE_AGE_SEARCH_BY_TITLE < Date.now())
.map(([title, entry]) => title);
}
getOutOfDateSeries(): string[] {
return Array.from(this._seriesById.entries())
.filter(([title, entry]) => entry.lastUpdateMs + this.MAX_CACHE_AGE_SERIES_BY_ID < Date.now())
.map(([title, entry]) => title);
}
getOutOfDateSeriesGroups(): string[] {
return Array.from(this._seriesGroupsById.entries())
.filter(([title, entry]) => entry.lastUpdateMs + this.MAX_CACHE_AGE_SERIES_GROUPS_BY_ID < Date.now())
.map(([title, entry]) => title);
}
}

View File

@@ -0,0 +1,106 @@
import {NextFunction, Request, Response} from 'express';
import {MangaUpdatesCache} from '../cache/MangaUpdatesCache.js';
export class MangaUpdatesController {
private cache: MangaUpdatesCache;
constructor(cache: MangaUpdatesCache) {
this.cache = cache;
}
async search(req: Request, res: Response, next: NextFunction): Promise<void> {
const bjson = req.body;
if (bjson['stype'] !== 'title' || bjson['type'] !== 'Manga' || !bjson['search']?.trim().length) {
res.status(400).send('Only {stype: "title", type: "Manga", search: "some title"} allowed!');
next('router');
return;
}
const fromCache = this.cache.getSearchByTitle(bjson['search']);
if (fromCache) {
res.status(200).setHeader('Content-Type', 'application/json').send(fromCache);
next('router');
return;
}
//throttle
await new Promise((r) => setTimeout(r, 1000));
//fetch from manga updates
const fromApi = await fetch('https://api.mangaupdates.com/v1/series/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(bjson),
});
if (fromApi.status !== 200) {
res.status(fromApi.status).send(fromApi.body);
next('router');
return;
}
const fromApiJson = await fromApi.text();
this.cache.putSearchByTitle(bjson['search'], fromApiJson);
res.status(200).setHeader('Content-Type', 'application/json').send(fromApiJson);
next('router');
}
async getById(req: Request, res: Response, next: NextFunction): Promise<void> {
const id = req.params.id!.toLowerCase();
const fromCache = this.cache.getSeriesById(id);
if (fromCache) {
res.status(200).setHeader('Content-Type', 'application/json').send(fromCache);
next();
return;
}
//throttle
await new Promise((r) => setTimeout(r, 1000));
//fetch from manga updates
const fromApi = await fetch('https://api.mangaupdates.com/v1/series/' + id);
if (fromApi.status !== 200) {
res.status(fromApi.status).send(fromApi.body);
next();
return;
}
const fromApiJson = await fromApi.text();
this.cache.putSeriesById(id, fromApiJson);
res.status(200).setHeader('Content-Type', 'application/json').send(fromApiJson);
next();
}
async getGroupById(req: Request, res: Response, next: NextFunction): Promise<void> {
const id = req.params.id!.toLowerCase();
const fromCache = this.cache.getSeriesGroupsById(id);
if (fromCache) {
res.status(200).setHeader('Content-Type', 'application/json').send(fromCache);
next();
return;
}
//throttle
await new Promise((r) => setTimeout(r, 1000));
//fetch from manga updates
const fromApi = await fetch('https://api.mangaupdates.com/v1/series/' + id + '/groups');
if (fromApi.status !== 200) {
res.status(fromApi.status).send(fromApi.body);
next();
return;
}
const fromApiJson = await fromApi.text();
this.cache.putSeriesGroupsById(id, fromApiJson);
res.status(200).setHeader('Content-Type', 'application/json').send(fromApiJson);
next();
}
}

29
backend/src/main.ts Normal file
View File

@@ -0,0 +1,29 @@
import compression from 'compression';
import express from 'express';
import aniListRouter from './router/AniListRouter.js';
import mangaUpdatesRouter from './router/MangaUpdatesRouter.js';
import Scheduler from './schedule/Scheduler.js';
import {MangaUpdatesCache} from './cache/MangaUpdatesCache.js';
import * as fs from 'fs';
const config = JSON.parse(fs.readFileSync('config.json').toString())
const app = express();
const mangaUpdatesCache = new MangaUpdatesCache();
const scheduler = new Scheduler(mangaUpdatesCache);
scheduler.registerJobs();
//middlewares
app.use(compression());
app.use(express.json());
//router
app.use('/anilist', aniListRouter());
app.use('/mangaupdates', mangaUpdatesRouter(mangaUpdatesCache));
app.use(express.static('_client')) //for production
//start
app.listen(config.port, config.host, () => {
console.log('Started server on http://' + config.host + ':' + config.port + '/');
});

View File

@@ -0,0 +1,24 @@
import {NextFunction, Request, Response, Router} from 'express';
export default function aniListRouter(): Router {
const router = Router();
router.post('/graphql', async (req: Request, res: Response, next: NextFunction) => {
try {
const fromApi = await fetch('https://graphql.anilist.co/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(req.body),
});
const fromApiText = await fromApi.text()
res.status(fromApi.status).send(fromApiText);
} catch (e) {
console.error(e);
}
next();
});
return router;
}

View File

@@ -0,0 +1,12 @@
import {Router} from 'express';
import {MangaUpdatesController} from '../controller/MangaUpdatesController.js';
import {MangaUpdatesCache} from '../cache/MangaUpdatesCache.js';
export default function mangaUpdatesRouter(cache: MangaUpdatesCache): Router {
const controller = new MangaUpdatesController(cache);
const router = Router();
router.post('/v1/series/search', controller.search.bind(controller));
router.get('/v1/series/:id', controller.getById.bind(controller));
router.get('/v1/series/:id/groups', controller.getGroupById.bind(controller));
return router;
}

View File

@@ -0,0 +1,4 @@
export default interface IJob<T> {
get schedule(): Date | string;
execute(): Promise<T>;
}

View File

@@ -0,0 +1,28 @@
import IJob from './IJob';
import MangaUpdateCacheRenewService from '../service/MangaUpdateCacheRenewService.js';
import {MangaUpdatesCache} from '../cache/MangaUpdatesCache.js';
export default class MangaUpdateCacheRenewJob implements IJob<void> {
private readonly service: MangaUpdateCacheRenewService;
private lock: boolean = false;
constructor(cache: MangaUpdatesCache) {
this.service = new MangaUpdateCacheRenewService(cache);
}
get schedule(): Date | string {
return '0 0 * * * *'; //every hour
}
async execute(): Promise<void> {
if (this.lock) {
return;
}
this.lock = true;
try {
await this.service.renew();
} finally {
this.lock = false;
}
}
}

View File

@@ -0,0 +1,22 @@
import {MangaUpdatesCache} from '../cache/MangaUpdatesCache.js';
import IJob from './IJob.js';
import MangaUpdateCacheRenewJob from './MangaUpdateCacheRenewJob.js';
import {gracefulShutdown, scheduleJob} from 'node-schedule';
export default class Scheduler {
private readonly jobs: IJob<any>[] = [];
constructor(cache: MangaUpdatesCache) {
this.jobs.push(
new MangaUpdateCacheRenewJob(cache),
);
}
registerJobs(): void {
this.jobs.forEach(e => scheduleJob(e.schedule, e.execute.bind(e)));
}
cancelJobs(): Promise<void> {
return gracefulShutdown();
}
}

View File

@@ -0,0 +1,86 @@
import {MangaUpdatesCache} from '../cache/MangaUpdatesCache';
export default class MangaUpdateCacheRenewService {
private static readonly delay = 3000;
private readonly cache: MangaUpdatesCache;
constructor(cache: MangaUpdatesCache) {
this.cache = cache;
}
async renew(): Promise<void> {
console.log('Renewing cache ...');
await this.renewRelations();
await this.renewSeries();
await this.renewUpdates();
console.log('Renewing cache done');
}
async renewRelations(): Promise<void> {
const titles = this.cache.getOutOfDateSearch();
console.log(titles.length + ' out-of-date relations');
for (let title of titles) {
await new Promise((r) => setTimeout(r, MangaUpdateCacheRenewService.delay));
try {
const fromApi = await fetch('https://api.mangaupdates.com/v1/series/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({stype: 'title', type: 'Manga', search: title}),
});
if (fromApi.status !== 200) {
continue;
}
const fromApiJson = await fromApi.text();
this.cache.putSearchByTitle(title, fromApiJson);
} catch (e) {
console.error(e);
}
}
}
async renewSeries(): Promise<void> {
const ids = this.cache.getOutOfDateSeries();
console.log(ids.length + ' out-of-date series');
for (let id of ids) {
await new Promise((r) => setTimeout(r, MangaUpdateCacheRenewService.delay));
try {
const fromApi = await fetch('https://api.mangaupdates.com/v1/series/' + id);
if (fromApi.status !== 200) {
continue;
}
const fromApiJson = await fromApi.text();
this.cache.putSeriesById(id, fromApiJson);
} catch (e) {
console.error(e);
}
}
}
async renewUpdates(): Promise<void> {
const ids = this.cache.getOutOfDateSeriesGroups();
console.log(ids.length + ' out-of-date series updates');
for (let id of ids) {
await new Promise((r) => setTimeout(r, MangaUpdateCacheRenewService.delay));
try {
const fromApi = await fetch('https://api.mangaupdates.com/v1/series/' + id + '/groups');
if (fromApi.status !== 200) {
continue;
}
const fromApiJson = await fromApi.text();
this.cache.putSeriesGroupsById(id, fromApiJson);
} catch (e) {
console.error(e);
}
}
}
}

111
backend/tsconfig.dev.json Normal file
View File

@@ -0,0 +1,111 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
// "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
// "module": "ESNext",
/* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dev", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
// "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
// "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
// "strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
// "skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

109
backend/tsconfig.json Normal file
View File

@@ -0,0 +1,109 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
"removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
"strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
"strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
"strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
"noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
"useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
"noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
"exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
"noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
"noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

24
build.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
cd "$(dirname "$0")"
mkdir dist
rm -rf dist/*
# build
(cd backend && npm i && npm run build) || exit 1
(cd frontend && npm i && npm run build-only) || exit 1
# copy backend
cp -r backend/dist/* dist
cp -r backend/node_modules dist
cp backend/config.json dist
# copy front end
mkdir dist/_client
cp -r frontend/dist/* dist/_client
# create start script
echo '#!/bin/bash' > dist/start.sh
echo 'cd "$(dirname "$0")"' >> dist/start.sh
echo 'node main.js' >> dist/start.sh
chmod +x dist/start.sh

View File

@@ -1,7 +1,9 @@
{ {
"name": "mangastatus", "name": "mangastatus-frontend",
"version": "0.0.0", "version": "1.0.0",
"private": true, "private": true,
"author": "wea_ondara",
"license": "ISC",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",

View File

@@ -4,7 +4,7 @@ import type {AniListMangaListCollection} from '@/data/models/anilist/AniListMang
export default class AniListApi { export default class AniListApi {
async fetchUser(userName: string): Promise<AniListUser> { async fetchUser(userName: string): Promise<AniListUser> {
const res = fetch('/graphql', { const res = fetch('/anilist/graphql', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -18,7 +18,7 @@ export default class AniListApi {
} }
async fetchManga(userId: number): Promise<AniListMangaListCollection> { async fetchManga(userId: number): Promise<AniListMangaListCollection> {
const res = fetch('/graphql', { const res = fetch('/anilist/graphql', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@@ -58,7 +58,7 @@ export default class MangaUpdatesDataService {
} }
continue; continue;
} finally { } finally {
await new Promise((r) => setTimeout(r, 1000)); // await new Promise((r) => setTimeout(r, 1000));
} }
matching = results.results matching = results.results
.filter(e => stringSimilarity(title, e.record.title, 2, false) >= 0.95) .filter(e => stringSimilarity(title, e.record.title, 2, false) >= 0.95)

View File

@@ -23,25 +23,14 @@ export default defineConfig({
}, },
clearScreen: false, clearScreen: false,
server: { server: {
proxy: { proxy: {//for dev
'/graphql': { '^/anilist/.*$': {
target: 'https://graphql.anilist.co', target: 'http://localhost:5000',
changeOrigin: true, changeOrigin: true,
configure: (proxy, options) => {
proxy.on('proxyRes', proxyRes => {
delete proxyRes.headers['set-cookie'];
});
},
}, },
'^/mangaupdates/.*$': { '^/mangaupdates/.*$': {
target: 'https://api.mangaupdates.com', target: 'http://localhost:5000',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/mangaupdates/, ''),
configure: (proxy, options) => {
proxy.on('proxyRes', proxyRes => {
delete proxyRes.headers['set-cookie'];
});
},
}, },
}, },
}, },