use MangaDex to better find links between AniList and MangaUpdates
This commit is contained in:
23
backend/src/cache/CacheUtil.ts
vendored
Normal file
23
backend/src/cache/CacheUtil.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export type CacheEntry = { data: string, lastUpdateMs: number }
|
||||||
|
|
||||||
|
export function 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
backend/src/cache/MangaDexCache.ts
vendored
Normal file
39
backend/src/cache/MangaDexCache.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import {CacheEntry, loadJson, saveJson} from './CacheUtil';
|
||||||
|
|
||||||
|
export class MangaDexCache {
|
||||||
|
private readonly FILE_CACHE_DIR = 'cache/mangadex';
|
||||||
|
private readonly FILE_SEARCH_BY_TITLE = this.FILE_CACHE_DIR + '/searchByTitle.json';
|
||||||
|
|
||||||
|
private readonly MAX_CACHE_AGE_SEARCH_BY_TITLE = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
private _searchByTitle = new Map<string, CacheEntry>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private load(): void {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(this.FILE_CACHE_DIR, {recursive: true});
|
||||||
|
} catch (_) {
|
||||||
|
}
|
||||||
|
this._searchByTitle = loadJson(this.FILE_SEARCH_BY_TITLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
putSearchByTitle(title: string, value: string): void {
|
||||||
|
this._searchByTitle.set(title, {data: value, lastUpdateMs: Date.now()});
|
||||||
|
saveJson(this.FILE_SEARCH_BY_TITLE, this._searchByTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
backend/src/cache/MangaUpdatesCache.ts
vendored
53
backend/src/cache/MangaUpdatesCache.ts
vendored
@@ -1,20 +1,22 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import {CacheEntry, loadJson, saveJson} from './CacheUtil';
|
||||||
type CacheEntry = { data: string, lastUpdateMs: number }
|
|
||||||
|
|
||||||
export class MangaUpdatesCache {
|
export class MangaUpdatesCache {
|
||||||
private readonly FILE_CACHE_DIR = 'cache';
|
private readonly FILE_CACHE_DIR = 'cache/mangaupdates';
|
||||||
private readonly FILE_SEARCH_BY_TITLE = this.FILE_CACHE_DIR + '/searchByTitle.json';
|
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_BY_ID = this.FILE_CACHE_DIR + '/seriesById.json';
|
||||||
private readonly FILE_SERIES_GROUPS_BY_ID = this.FILE_CACHE_DIR + '/seriesGroupsById.json';
|
private readonly FILE_SERIES_GROUPS_BY_ID = this.FILE_CACHE_DIR + '/seriesGroupsById.json';
|
||||||
|
private readonly FILE_SERIES_IDS_BY_WEBSITE_ID = this.FILE_CACHE_DIR + '/seriesIdsByWebsiteId.json';
|
||||||
|
|
||||||
private readonly MAX_CACHE_AGE_SEARCH_BY_TITLE = 7 * 24 * 60 * 60 * 1000;
|
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_BY_ID = 30 * 24 * 60 * 60 * 1000;
|
||||||
private readonly MAX_CACHE_AGE_SERIES_GROUPS_BY_ID = 1 * 24 * 60 * 60 * 1000;
|
private readonly MAX_CACHE_AGE_SERIES_GROUPS_BY_ID = 1 * 24 * 60 * 60 * 1000;
|
||||||
|
private readonly MAX_CACHE_AGE_SERIES_IDS_BY_WEBSITE_ID = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
private _searchByTitle = new Map<string, CacheEntry>();
|
private _searchByTitle = new Map<string, CacheEntry>();
|
||||||
private _seriesById = new Map<string, CacheEntry>();
|
private _seriesById = new Map<string, CacheEntry>();
|
||||||
private _seriesGroupsById = new Map<string, CacheEntry>();
|
private _seriesGroupsById = new Map<string, CacheEntry>();
|
||||||
|
private _seriesIdsByWebsiteId = new Map<string, CacheEntry>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.load();
|
this.load();
|
||||||
@@ -22,32 +24,13 @@ export class MangaUpdatesCache {
|
|||||||
|
|
||||||
private load(): void {
|
private load(): void {
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(this.FILE_CACHE_DIR);
|
fs.mkdirSync(this.FILE_CACHE_DIR, {recursive: true});
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
}
|
}
|
||||||
this._searchByTitle = this.loadJson(this.FILE_SEARCH_BY_TITLE);
|
this._searchByTitle = loadJson(this.FILE_SEARCH_BY_TITLE);
|
||||||
this._seriesById = this.loadJson(this.FILE_SERIES_BY_ID);
|
this._seriesById = loadJson(this.FILE_SERIES_BY_ID);
|
||||||
this._seriesGroupsById = this.loadJson(this.FILE_SERIES_GROUPS_BY_ID);
|
this._seriesGroupsById = loadJson(this.FILE_SERIES_GROUPS_BY_ID);
|
||||||
}
|
this._seriesIdsByWebsiteId = loadJson(this.FILE_SERIES_IDS_BY_WEBSITE_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 {
|
getSearchByTitle(title: string): string | undefined {
|
||||||
@@ -65,19 +48,29 @@ export class MangaUpdatesCache {
|
|||||||
return !entry || entry.lastUpdateMs + this.MAX_CACHE_AGE_SERIES_GROUPS_BY_ID < Date.now() ? undefined : entry.data;
|
return !entry || entry.lastUpdateMs + this.MAX_CACHE_AGE_SERIES_GROUPS_BY_ID < Date.now() ? undefined : entry.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSeriesIdByWebsiteId(id: string): string | undefined {
|
||||||
|
const entry = this._seriesIdsByWebsiteId.get(id);
|
||||||
|
return !entry || entry.lastUpdateMs + this.MAX_CACHE_AGE_SERIES_IDS_BY_WEBSITE_ID < Date.now() ? undefined : entry.data;
|
||||||
|
}
|
||||||
|
|
||||||
putSearchByTitle(title: string, value: string): void {
|
putSearchByTitle(title: string, value: string): void {
|
||||||
this._searchByTitle.set(title, {data: value, lastUpdateMs: Date.now()});
|
this._searchByTitle.set(title, {data: value, lastUpdateMs: Date.now()});
|
||||||
this.saveJson(this.FILE_SEARCH_BY_TITLE, this._searchByTitle);
|
saveJson(this.FILE_SEARCH_BY_TITLE, this._searchByTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
putSeriesById(id: string, value: string): void {
|
putSeriesById(id: string, value: string): void {
|
||||||
this._seriesById.set(id, {data: value, lastUpdateMs: Date.now()});
|
this._seriesById.set(id, {data: value, lastUpdateMs: Date.now()});
|
||||||
this.saveJson(this.FILE_SERIES_BY_ID, this._seriesById);
|
saveJson(this.FILE_SERIES_BY_ID, this._seriesById);
|
||||||
}
|
}
|
||||||
|
|
||||||
putSeriesGroupsById(id: string, value: string): void {
|
putSeriesGroupsById(id: string, value: string): void {
|
||||||
this._seriesGroupsById.set(id, {data: value, lastUpdateMs: Date.now()});
|
this._seriesGroupsById.set(id, {data: value, lastUpdateMs: Date.now()});
|
||||||
this.saveJson(this.FILE_SERIES_GROUPS_BY_ID, this._seriesGroupsById);
|
saveJson(this.FILE_SERIES_GROUPS_BY_ID, this._seriesGroupsById);
|
||||||
|
}
|
||||||
|
|
||||||
|
putSeriesIdByWebsiteId(id: string, value: string): void {
|
||||||
|
this._seriesIdsByWebsiteId.set(id, {data: value, lastUpdateMs: Date.now()});
|
||||||
|
saveJson(this.FILE_SERIES_IDS_BY_WEBSITE_ID, this._seriesIdsByWebsiteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getOutOfDateSearch(): string[] {
|
getOutOfDateSearch(): string[] {
|
||||||
|
|||||||
47
backend/src/controller/MangaDexController.ts
Normal file
47
backend/src/controller/MangaDexController.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import {NextFunction, Request, Response} from 'express';
|
||||||
|
import {MangaDexCache} from '../cache/MangaDexCache';
|
||||||
|
|
||||||
|
export class MangaDexController {
|
||||||
|
private cache: MangaDexCache;
|
||||||
|
|
||||||
|
constructor(cache: MangaDexCache) {
|
||||||
|
this.cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
async manga(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
try {
|
||||||
|
const title = req.query.title as string | undefined;
|
||||||
|
if (!title || !title.trim().length) {
|
||||||
|
res.status(400).send('Title required!');
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromCache = this.cache.getSearchByTitle(title);
|
||||||
|
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.mangadex.org/manga?title=' + encodeURIComponent(title));
|
||||||
|
if (fromApi.status !== 200) {
|
||||||
|
res.status(fromApi.status).send(fromApi.body);
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromApiJson = await fromApi.text();
|
||||||
|
this.cache.putSearchByTitle(title, fromApiJson);
|
||||||
|
|
||||||
|
res.status(200).setHeader('Content-Type', 'application/json').send(fromApiJson);
|
||||||
|
next();
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,4 +103,50 @@ export class MangaUpdatesController {
|
|||||||
res.status(200).setHeader('Content-Type', 'application/json').send(fromApiJson);
|
res.status(200).setHeader('Content-Type', 'application/json').send(fromApiJson);
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSeriesIdFromWebsiteId(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
try {
|
||||||
|
const id = req.params.websiteId;
|
||||||
|
if (!id || !id.trim().length || !id.match(/^[0-9a-zA-Z]+$/)) {
|
||||||
|
res.status(400).send('Website id required!');
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromCache = this.cache.getSeriesIdByWebsiteId(id);
|
||||||
|
if (fromCache) {
|
||||||
|
res.status(200).send(fromCache);
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//throttle
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
|
||||||
|
//fetch from manga updates
|
||||||
|
const fromApi = await fetch(id.match(/^[0-9]+$/)
|
||||||
|
? 'https://www.mangaupdates.com/series.html?id=' + id
|
||||||
|
: 'https://www.mangaupdates.com/series/' + id);
|
||||||
|
if (fromApi.status !== 200) {
|
||||||
|
res.status(fromApi.status).send(fromApi.body);
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromApiHtml = await fromApi.text();
|
||||||
|
const match = fromApiHtml.match(/https:\/\/api.mangaupdates.com\/v1\/series\/([0-9]+)\/rss/);
|
||||||
|
if (!match) {
|
||||||
|
res.status(404).send('Series id not found in website!');
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const json = JSON.stringify({website_id: id, series_id: parseInt(match[1]!)});
|
||||||
|
this.cache.putSeriesIdByWebsiteId(id, json);
|
||||||
|
|
||||||
|
res.status(200).send(json);
|
||||||
|
next();
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,12 +5,15 @@ import mangaUpdatesRouter from './router/MangaUpdatesRouter.js';
|
|||||||
import Scheduler from './schedule/Scheduler.js';
|
import Scheduler from './schedule/Scheduler.js';
|
||||||
import {MangaUpdatesCache} from './cache/MangaUpdatesCache.js';
|
import {MangaUpdatesCache} from './cache/MangaUpdatesCache.js';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import {MangaDexCache} from './cache/MangaDexCache';
|
||||||
|
import mangaDexRouter from './router/MangaDexRouter';
|
||||||
|
|
||||||
const config = JSON.parse(fs.readFileSync('config.json').toString())
|
const config = JSON.parse(fs.readFileSync('config.json').toString())
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
const mangaDexCache = new MangaDexCache();
|
||||||
const mangaUpdatesCache = new MangaUpdatesCache();
|
const mangaUpdatesCache = new MangaUpdatesCache();
|
||||||
const scheduler = new Scheduler(mangaUpdatesCache);
|
const scheduler = new Scheduler(mangaDexCache, mangaUpdatesCache);
|
||||||
|
|
||||||
scheduler.registerJobs();
|
scheduler.registerJobs();
|
||||||
|
|
||||||
@@ -20,6 +23,7 @@ app.use(express.json());
|
|||||||
|
|
||||||
//router
|
//router
|
||||||
app.use('/anilist', aniListRouter());
|
app.use('/anilist', aniListRouter());
|
||||||
|
app.use('/mangadex', mangaDexRouter(mangaDexCache));
|
||||||
app.use('/mangaupdates', mangaUpdatesRouter(mangaUpdatesCache));
|
app.use('/mangaupdates', mangaUpdatesRouter(mangaUpdatesCache));
|
||||||
app.use(express.static('_client')) //for production
|
app.use(express.static('_client')) //for production
|
||||||
|
|
||||||
|
|||||||
10
backend/src/router/MangaDexRouter.ts
Normal file
10
backend/src/router/MangaDexRouter.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import {Router} from 'express';
|
||||||
|
import {MangaDexController} from '../controller/MangaDexController';
|
||||||
|
import {MangaDexCache} from '../cache/MangaDexCache';
|
||||||
|
|
||||||
|
export default function mangaDexRouter(cache: MangaDexCache): Router {
|
||||||
|
const controller = new MangaDexController(cache);
|
||||||
|
const router = Router();
|
||||||
|
router.get('/manga', controller.manga.bind(controller));
|
||||||
|
return router;
|
||||||
|
}
|
||||||
@@ -8,5 +8,6 @@ export default function mangaUpdatesRouter(cache: MangaUpdatesCache): Router {
|
|||||||
router.post('/v1/series/search', controller.search.bind(controller));
|
router.post('/v1/series/search', controller.search.bind(controller));
|
||||||
router.get('/v1/series/:id', controller.getById.bind(controller));
|
router.get('/v1/series/:id', controller.getById.bind(controller));
|
||||||
router.get('/v1/series/:id/groups', controller.getGroupById.bind(controller));
|
router.get('/v1/series/:id/groups', controller.getGroupById.bind(controller));
|
||||||
|
router.get('/series_id_from_website_id/:websiteId', controller.getSeriesIdFromWebsiteId.bind(controller));
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
28
backend/src/schedule/MangaDexCacheRenewJob.ts
Normal file
28
backend/src/schedule/MangaDexCacheRenewJob.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import IJob from './IJob';
|
||||||
|
import MangaDexCacheRenewService from '../service/MangaDexCacheRenewService';
|
||||||
|
import {MangaDexCache} from '../cache/MangaDexCache';
|
||||||
|
|
||||||
|
export default class MangaDexCacheRenewJob implements IJob<void> {
|
||||||
|
private readonly service: MangaDexCacheRenewService;
|
||||||
|
private lock: boolean = false;
|
||||||
|
|
||||||
|
constructor(cache: MangaDexCache) {
|
||||||
|
this.service = new MangaDexCacheRenewService(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,16 @@ import {MangaUpdatesCache} from '../cache/MangaUpdatesCache.js';
|
|||||||
import IJob from './IJob.js';
|
import IJob from './IJob.js';
|
||||||
import MangaUpdateCacheRenewJob from './MangaUpdateCacheRenewJob.js';
|
import MangaUpdateCacheRenewJob from './MangaUpdateCacheRenewJob.js';
|
||||||
import {gracefulShutdown, scheduleJob} from 'node-schedule';
|
import {gracefulShutdown, scheduleJob} from 'node-schedule';
|
||||||
|
import {MangaDexCache} from '../cache/MangaDexCache';
|
||||||
|
import MangaDexCacheRenewJob from './MangaDexCacheRenewJob';
|
||||||
|
|
||||||
export default class Scheduler {
|
export default class Scheduler {
|
||||||
private readonly jobs: IJob<any>[] = [];
|
private readonly jobs: IJob<any>[] = [];
|
||||||
|
|
||||||
constructor(cache: MangaUpdatesCache) {
|
constructor(mangaDexCache: MangaDexCache, mangaUpdatesCache: MangaUpdatesCache) {
|
||||||
this.jobs.push(
|
this.jobs.push(
|
||||||
new MangaUpdateCacheRenewJob(cache),
|
new MangaDexCacheRenewJob(mangaDexCache),
|
||||||
|
new MangaUpdateCacheRenewJob(mangaUpdatesCache),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
backend/src/service/MangaDexCacheRenewService.ts
Normal file
37
backend/src/service/MangaDexCacheRenewService.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import {MangaDexCache} from '../cache/MangaDexCache';
|
||||||
|
|
||||||
|
export default class MangaDexCacheRenewService {
|
||||||
|
private static readonly delay = 3000;
|
||||||
|
|
||||||
|
private readonly cache: MangaDexCache;
|
||||||
|
|
||||||
|
constructor(cache: MangaDexCache) {
|
||||||
|
this.cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
async renew(): Promise<void> {
|
||||||
|
console.log('Renewing MangaDex cache ...');
|
||||||
|
await this.renewMedia();
|
||||||
|
console.log('Renewing MangaDex cache done');
|
||||||
|
}
|
||||||
|
|
||||||
|
async renewMedia(): Promise<void> {
|
||||||
|
const titles = this.cache.getOutOfDateSearch();
|
||||||
|
console.log(titles.length + ' out-of-date media');
|
||||||
|
|
||||||
|
for (let title of titles) {
|
||||||
|
await new Promise((r) => setTimeout(r, MangaDexCacheRenewService.delay));
|
||||||
|
try {
|
||||||
|
const fromApi = await fetch('https://api.mangadex.org/manga?title=' + encodeURIComponent(title));
|
||||||
|
if (fromApi.status !== 200) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromApiJson = await fromApi.text();
|
||||||
|
this.cache.putSearchByTitle(title, fromApiJson);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,11 +10,11 @@ export default class MangaUpdateCacheRenewService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async renew(): Promise<void> {
|
async renew(): Promise<void> {
|
||||||
console.log('Renewing cache ...');
|
console.log('Renewing MangaUpdates cache ...');
|
||||||
await this.renewRelations();
|
await this.renewRelations();
|
||||||
await this.renewSeries();
|
await this.renewSeries();
|
||||||
await this.renewUpdates();
|
await this.renewUpdates();
|
||||||
console.log('Renewing cache done');
|
console.log('Renewing MangaUpdates cache done');
|
||||||
}
|
}
|
||||||
|
|
||||||
async renewRelations(): Promise<void> {
|
async renewRelations(): Promise<void> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Progress} from '@/data/service/MangaUpdatesDataService';
|
import {Progress} from '@/data/service/Progress';
|
||||||
import {Options, Vue} from 'vue-class-component';
|
import {Options, Vue} from 'vue-class-component';
|
||||||
import {ServiceStore} from '@/stores/ServiceStore';
|
import {ServiceStore} from '@/stores/ServiceStore';
|
||||||
import {BSpinner} from 'bootstrap-vue-next';
|
import {BSpinner} from 'bootstrap-vue-next';
|
||||||
@@ -13,7 +13,8 @@ import {toast} from 'vue3-toastify';
|
|||||||
})
|
})
|
||||||
export default class MangaUpdatesUpdater extends Vue {
|
export default class MangaUpdatesUpdater extends Vue {
|
||||||
//vars
|
//vars
|
||||||
progress: Progress = new Progress(this.onProgress.bind(this), this.onFinished.bind(this));
|
progressMangaDex: Progress = new Progress(this.onProgress.bind(this), () => {});
|
||||||
|
progressMangaUpdates: Progress = new Progress(this.onProgress.bind(this), this.onFinished.bind(this));
|
||||||
progressType: string | null = null;
|
progressType: string | null = null;
|
||||||
progressValue: number | null = null;
|
progressValue: number | null = null;
|
||||||
progressMax: number | null = null;
|
progressMax: number | null = null;
|
||||||
@@ -35,9 +36,10 @@ export default class MangaUpdatesUpdater extends Vue {
|
|||||||
|
|
||||||
//event handler
|
//event handler
|
||||||
async onUpdateMangaUpdatesDb(): Promise<void> {
|
async onUpdateMangaUpdatesDb(): Promise<void> {
|
||||||
this.progressType = 'starting';
|
this.progressType = 'general.starting';
|
||||||
await this.tryAcquireWakeLock();
|
await this.tryAcquireWakeLock();
|
||||||
this.serviceStore.mangaUpdatesDataService.updateDb(this.progress);
|
await this.serviceStore.mangaDexDataService.updateDb(this.progressMangaDex);
|
||||||
|
await this.serviceStore.mangaUpdatesDataService.updateDb(this.progressMangaUpdates);
|
||||||
};
|
};
|
||||||
|
|
||||||
onProgress(type: string, progress: number, max: number): void {
|
onProgress(type: string, progress: number, max: number): void {
|
||||||
@@ -50,7 +52,7 @@ export default class MangaUpdatesUpdater extends Vue {
|
|||||||
this.wakeLock?.release();
|
this.wakeLock?.release();
|
||||||
this.wakeLock = null;
|
this.wakeLock = null;
|
||||||
|
|
||||||
this.progressType = 'finished';
|
this.progressType = 'general.finished';
|
||||||
this.progressValue = null;
|
this.progressValue = null;
|
||||||
this.progressMax = null;
|
this.progressMax = null;
|
||||||
|
|
||||||
@@ -89,10 +91,10 @@ export default class MangaUpdatesUpdater extends Vue {
|
|||||||
<span class="text-nowrap">
|
<span class="text-nowrap">
|
||||||
<BSpinner small class="me-2"/>
|
<BSpinner small class="me-2"/>
|
||||||
<template v-if="progressValue === null || progressMax === null">
|
<template v-if="progressValue === null || progressMax === null">
|
||||||
{{ $t('fetch.mangaUpdates.' + progressType) }}
|
{{ $t('fetch.' + progressType) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ $t('fetch.mangaUpdates.' + progressType) + ': ' + (progressValue ?? 0) + '/' + (progressMax ?? 1) }}
|
{{ $t('fetch.' + progressType) + ': ' + (progressValue ?? 0) + '/' + (progressMax ?? 1) }}
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
9
frontend/src/data/api/MangaDexApi.ts
Normal file
9
frontend/src/data/api/MangaDexApi.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import {handleJsonResponse} from '@/data/api/ApiUtils';
|
||||||
|
import type {MangaDexSearchResult} from '@/data/models/mangadex/MangaDexSearchResult';
|
||||||
|
|
||||||
|
export default class MangaDexApi {
|
||||||
|
search(name: string): Promise<MangaDexSearchResult> {
|
||||||
|
const res = fetch('/mangadex/manga?title=' + encodeURIComponent(name));
|
||||||
|
return handleJsonResponse(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@ import {handleJsonResponse} from '@/data/api/ApiUtils';
|
|||||||
import type {MangaUpdatesSearchResult} from '@/data/models/mangaupdates/MangaUpdatesSearchResult';
|
import type {MangaUpdatesSearchResult} from '@/data/models/mangaupdates/MangaUpdatesSearchResult';
|
||||||
import type {MangaUpdatesSeries} from '@/data/models/mangaupdates/MangaUpdatesSeries';
|
import type {MangaUpdatesSeries} from '@/data/models/mangaupdates/MangaUpdatesSeries';
|
||||||
import type {MangaUpdatesSeriesGroups} from '@/data/models/mangaupdates/MangaUpdatesSeriesGroups';
|
import type {MangaUpdatesSeriesGroups} from '@/data/models/mangaupdates/MangaUpdatesSeriesGroups';
|
||||||
|
import type {
|
||||||
|
MangaUpdatesWebsiteIdSeriesIdRelation
|
||||||
|
} from '@/data/models/mangaupdates/MangaUpdatesWebsiteIdSeriesIdRelation';
|
||||||
|
|
||||||
export default class MangaUpdatesApi {
|
export default class MangaUpdatesApi {
|
||||||
search(name: string): Promise<MangaUpdatesSearchResult> {
|
search(name: string): Promise<MangaUpdatesSearchResult> {
|
||||||
@@ -27,4 +30,9 @@ export default class MangaUpdatesApi {
|
|||||||
const res = fetch(`/mangaupdates/v1/series/${id}/groups`);
|
const res = fetch(`/mangaupdates/v1/series/${id}/groups`);
|
||||||
return handleJsonResponse(res);
|
return handleJsonResponse(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
seriesIdByWebsiteId(websiteId: number): Promise<MangaUpdatesWebsiteIdSeriesIdRelation> {
|
||||||
|
const res = fetch('/mangaupdates/series_id_from_website_id/' + websiteId);
|
||||||
|
return handleJsonResponse(res);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
frontend/src/data/models/mangadex/MangaDexMedia.ts
Normal file
66
frontend/src/data/models/mangadex/MangaDexMedia.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
export type MangaDexMedia = {
|
||||||
|
id: string, //uuid
|
||||||
|
type: 'manga' | string,
|
||||||
|
attributes: {
|
||||||
|
title: {
|
||||||
|
//2 letter language code: name
|
||||||
|
[key: string]: string,
|
||||||
|
},
|
||||||
|
altTitles: {
|
||||||
|
//2 letter language code: name
|
||||||
|
[key: string]: string
|
||||||
|
}[],
|
||||||
|
description: {
|
||||||
|
//2 letter language code: description
|
||||||
|
[key: string]: string
|
||||||
|
},
|
||||||
|
isLocked: boolean,
|
||||||
|
links?: {
|
||||||
|
al?: string, //anilist id
|
||||||
|
ap?: string, //human-readable title,
|
||||||
|
bw?: string, //'series/<id>',
|
||||||
|
kt?: string, //kt id
|
||||||
|
mu?: string, //mangaupdates id
|
||||||
|
nu?: string, //human-readable title,
|
||||||
|
amz?: string, //url
|
||||||
|
ebj?: string, //url
|
||||||
|
mal?: string, //myanimelist id
|
||||||
|
raw?: string, //url
|
||||||
|
engtl?: string, //url
|
||||||
|
[key: string]: string | undefined,
|
||||||
|
},
|
||||||
|
originalLanguage: string,//2 letter language code
|
||||||
|
lastVolume: string,
|
||||||
|
lastChapter: string,
|
||||||
|
publicationDemographic: 'shounen' | string,
|
||||||
|
status: 'ongoing' | string,
|
||||||
|
year: number,
|
||||||
|
contentRating: 'suggestive' | string,
|
||||||
|
tags: {
|
||||||
|
id: string, //uuid
|
||||||
|
type: 'tag',
|
||||||
|
attributes: {
|
||||||
|
name: {
|
||||||
|
//2 letter language code: name
|
||||||
|
[key: string]: string
|
||||||
|
},
|
||||||
|
description: {},
|
||||||
|
group: 'theme' | string,
|
||||||
|
version: number
|
||||||
|
},
|
||||||
|
relationships: []
|
||||||
|
}[],
|
||||||
|
state: 'published' | string,
|
||||||
|
chapterNumbersResetOnNewVolume: boolean,
|
||||||
|
createdAt: string, //iso date string
|
||||||
|
updatedAt: string, //iso date string
|
||||||
|
version: number,
|
||||||
|
availableTranslatedLanguages: string[], //2 letter language code
|
||||||
|
latestUploadedChapter: string, //uuid
|
||||||
|
},
|
||||||
|
relationships: {
|
||||||
|
id: string, //uuid
|
||||||
|
type: 'author' | 'artist' | 'cover_art' | 'adapted_from' | 'side_story' | string,
|
||||||
|
related?: 'main_story' | string,
|
||||||
|
}[]
|
||||||
|
}
|
||||||
10
frontend/src/data/models/mangadex/MangaDexSearchResult.ts
Normal file
10
frontend/src/data/models/mangadex/MangaDexSearchResult.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type {MangaDexMedia} from '@/data/models/mangadex/MangaDexMedia';
|
||||||
|
|
||||||
|
export type MangaDexSearchResult = {
|
||||||
|
result: 'ok' | string,
|
||||||
|
response: 'collection' | string,
|
||||||
|
data: MangaDexMedia[],
|
||||||
|
limit: number,
|
||||||
|
offset: number,
|
||||||
|
total: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type MangaUpdatesWebsiteIdSeriesIdRelation = {
|
||||||
|
website_id: string;
|
||||||
|
series_id: number;
|
||||||
|
}
|
||||||
84
frontend/src/data/service/MangaDexDataService.ts
Normal file
84
frontend/src/data/service/MangaDexDataService.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import {MangaStore} from '@/stores/MangaStore';
|
||||||
|
import {DbStore} from '@/stores/DbStore';
|
||||||
|
import {ApiError} from '@/data/api/ApiUtils';
|
||||||
|
import MangaDexApi from '@/data/api/MangaDexApi';
|
||||||
|
import {Progress} from '@/data/service/Progress';
|
||||||
|
import MangaUpdatesApi from '@/data/api/MangaUpdatesApi';
|
||||||
|
import type {MangaUpdatesRelation} from '@/data/models/mangaupdates/MangaUpdatesRelation';
|
||||||
|
|
||||||
|
export default class MangaDexDataService {
|
||||||
|
private readonly mangaDexApi = new MangaDexApi();
|
||||||
|
private readonly mangaUpdatesApi = new MangaUpdatesApi();
|
||||||
|
|
||||||
|
updateDb(progress: Progress): Promise<void> {
|
||||||
|
const mangaStore = new MangaStore();
|
||||||
|
const dbStore = new DbStore();
|
||||||
|
return this.findMissingRelations(mangaStore, dbStore, progress)
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
.then(_ => progress.onFinished());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findMissingRelations(mangaStore: MangaStore, dbStore: DbStore, progress: Progress): Promise<void> {
|
||||||
|
const allowedTypes = new Set(['manga'].map(e => e.toLowerCase()));
|
||||||
|
|
||||||
|
const media = await dbStore.aniListMangaRepository.getMedia();
|
||||||
|
const relations = await dbStore.mangaUpdatesRepository.getRelations();
|
||||||
|
const presentRelationIds = new Set(relations.map(e => e.aniListMediaId));
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
for (const m of media) {
|
||||||
|
try {
|
||||||
|
if (presentRelationIds.has(m.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let matching: MangaUpdatesRelation[] = [];
|
||||||
|
for (let title of [m.title.native, m.title.romaji, m.title.english]) {
|
||||||
|
if (!title?.trim().length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let results;
|
||||||
|
try {
|
||||||
|
results = await this.mangaDexApi.search(title);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError && [400, 500].includes(err.statusCode)) {
|
||||||
|
console.debug(err);
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
matching = results.data
|
||||||
|
.filter(e => allowedTypes.has(e.type))
|
||||||
|
.filter(e => e.attributes.links?.al?.match(/[0-9]+/) && e.attributes.links.mu)
|
||||||
|
.filter(e => parseInt(e.attributes.links!.al!) === m.id)
|
||||||
|
.map(e => ({
|
||||||
|
aniListMediaId: parseInt(e.attributes.links!.al!),
|
||||||
|
mangaUpdatesSeriesId: e.attributes.links!.mu! as any as number, //TODO cast is hack, make pretty
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (let j = 0; j < matching.length; j++) {
|
||||||
|
try {
|
||||||
|
const websiteIdSeriesIdRelation = await this.mangaUpdatesApi.seriesIdByWebsiteId(matching[j].mangaUpdatesSeriesId);
|
||||||
|
matching[j].mangaUpdatesSeriesId = websiteIdSeriesIdRelation.series_id;
|
||||||
|
} catch(_) {
|
||||||
|
matching.splice(j--, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matching.length > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matching.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mangaStore.addMangaUpdatesRelations([matching[0]]);
|
||||||
|
} finally {
|
||||||
|
await progress.onProgress('mangaDex.relations', ++i, media.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,14 +7,15 @@ import stringSimilarity from 'string-similarity-js';
|
|||||||
import {ApiError} from '@/data/api/ApiUtils';
|
import {ApiError} from '@/data/api/ApiUtils';
|
||||||
import groupBy from '@/util';
|
import groupBy from '@/util';
|
||||||
import {decode} from 'html-entities';
|
import {decode} from 'html-entities';
|
||||||
|
import {Progress} from '@/data/service/Progress';
|
||||||
|
|
||||||
export default class MangaUpdatesDataService {
|
export default class MangaUpdatesDataService {
|
||||||
private readonly mangaUpdatesApi = new MangaUpdatesApi();
|
private readonly mangaUpdatesApi = new MangaUpdatesApi();
|
||||||
|
|
||||||
updateDb(progress: Progress): Progress {
|
updateDb(progress: Progress): Promise<void> {
|
||||||
const mangaStore = new MangaStore();
|
const mangaStore = new MangaStore();
|
||||||
const dbStore = new DbStore();
|
const dbStore = new DbStore();
|
||||||
this.findMissingRelations(mangaStore, dbStore, progress)
|
return this.findMissingRelations(mangaStore, dbStore, progress)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
})
|
})
|
||||||
@@ -27,7 +28,6 @@ export default class MangaUpdatesDataService {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
})
|
})
|
||||||
.then(_ => progress.onFinished());
|
.then(_ => progress.onFinished());
|
||||||
return progress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async findMissingRelations(mangaStore: MangaStore, dbStore: DbStore, progress: Progress): Promise<void> {
|
private async findMissingRelations(mangaStore: MangaStore, dbStore: DbStore, progress: Progress): Promise<void> {
|
||||||
@@ -79,7 +79,7 @@ export default class MangaUpdatesDataService {
|
|||||||
|
|
||||||
await mangaStore.addMangaUpdatesRelations([{aniListMediaId: m.id, mangaUpdatesSeriesId: matching[0].record.series_id}]);
|
await mangaStore.addMangaUpdatesRelations([{aniListMediaId: m.id, mangaUpdatesSeriesId: matching[0].record.series_id}]);
|
||||||
} finally {
|
} finally {
|
||||||
await progress.onProgress('relations', ++i, media.length);
|
await progress.onProgress('mangaUpdates.relations', ++i, media.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ export default class MangaUpdatesDataService {
|
|||||||
}
|
}
|
||||||
await mangaStore.updateMangaUpdatesSeries([series]);
|
await mangaStore.updateMangaUpdatesSeries([series]);
|
||||||
} finally {
|
} finally {
|
||||||
await progress.onProgress('series', ++i, relations.length);
|
await progress.onProgress('mangaUpdates.series', ++i, relations.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,30 +148,10 @@ export default class MangaUpdatesDataService {
|
|||||||
.map(chaptersOfGroup => chaptersOfGroup.reduce((l, r) => l.chapter > r.chapter ? l : r, chaptersOfGroup[0]));
|
.map(chaptersOfGroup => chaptersOfGroup.reduce((l, r) => l.chapter > r.chapter ? l : r, chaptersOfGroup[0]));
|
||||||
cachedChapterUpdates.push(...filtered)
|
cachedChapterUpdates.push(...filtered)
|
||||||
} finally {
|
} finally {
|
||||||
await progress.onProgress('chapters', ++i, series.length);
|
await progress.onProgress('mangaUpdates.chapters', ++i, series.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await mangaStore.updateMangaUpdatesChapters(cachedChapterUpdates);
|
await mangaStore.updateMangaUpdatesChapters(cachedChapterUpdates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProgressCallBackFn = (type: string, progress: number, max: number) => (Promise<void> | void);
|
|
||||||
export type FinishCallBackFn = () => (Promise<void> | void);
|
|
||||||
|
|
||||||
export class Progress {
|
|
||||||
readonly progress: ProgressCallBackFn;
|
|
||||||
readonly finish: FinishCallBackFn;
|
|
||||||
|
|
||||||
constructor(onProgress: ProgressCallBackFn, onFinish: FinishCallBackFn) {
|
|
||||||
this.progress = onProgress;
|
|
||||||
this.finish = onFinish;
|
|
||||||
}
|
|
||||||
|
|
||||||
async onProgress(type: string, progress: number, max: number): Promise<void> {
|
|
||||||
return (this.progress ? this.progress(type, progress, max) : Promise.resolve());
|
|
||||||
}
|
|
||||||
|
|
||||||
async onFinished(): Promise<void> {
|
|
||||||
return (this.finish ? this.finish() : Promise.resolve());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
20
frontend/src/data/service/Progress.ts
Normal file
20
frontend/src/data/service/Progress.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export type ProgressCallBackFn = (type: string, progress: number, max: number) => (Promise<void> | void);
|
||||||
|
export type FinishCallBackFn = () => (Promise<void> | void);
|
||||||
|
|
||||||
|
export class Progress {
|
||||||
|
readonly progress: ProgressCallBackFn;
|
||||||
|
readonly finish: FinishCallBackFn;
|
||||||
|
|
||||||
|
constructor(onProgress: ProgressCallBackFn, onFinish: FinishCallBackFn) {
|
||||||
|
this.progress = onProgress;
|
||||||
|
this.finish = onFinish;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onProgress(type: string, progress: number, max: number): Promise<void> {
|
||||||
|
return (this.progress ? this.progress(type, progress, max) : Promise.resolve());
|
||||||
|
}
|
||||||
|
|
||||||
|
async onFinished(): Promise<void> {
|
||||||
|
return (this.finish ? this.finish() : Promise.resolve());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,17 @@ export const messagesDe = {
|
|||||||
},
|
},
|
||||||
design: 'Design',
|
design: 'Design',
|
||||||
fetch: {
|
fetch: {
|
||||||
|
general: {
|
||||||
|
finished: 'Fertig',
|
||||||
|
starting: 'Starte',
|
||||||
|
},
|
||||||
|
mangaDex: {
|
||||||
|
relations: 'Relationen',
|
||||||
|
},
|
||||||
mangaUpdates: {
|
mangaUpdates: {
|
||||||
chapters: 'Kapitel',
|
chapters: 'Kapitel',
|
||||||
finished: 'Fertig',
|
|
||||||
relations: 'Relationen',
|
relations: 'Relationen',
|
||||||
series: 'Kapitel',
|
series: 'Kapitel',
|
||||||
starting: 'Starte',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
locale: 'Sprache',
|
locale: 'Sprache',
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ export const messagesEn = {
|
|||||||
},
|
},
|
||||||
design: 'Design',
|
design: 'Design',
|
||||||
fetch: {
|
fetch: {
|
||||||
|
general: {
|
||||||
|
finished: 'Finished',
|
||||||
|
starting: 'Starting',
|
||||||
|
},
|
||||||
|
mangaDex: {
|
||||||
|
relations: 'Relations',
|
||||||
|
},
|
||||||
mangaUpdates: {
|
mangaUpdates: {
|
||||||
chapters: 'Chapters',
|
chapters: 'Chapters',
|
||||||
finished: 'Finished',
|
|
||||||
relations: 'Relations',
|
relations: 'Relations',
|
||||||
series: 'Series',
|
series: 'Series',
|
||||||
starting: 'Starting',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
locale: 'Language',
|
locale: 'Language',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {Pinia, Store} from 'pinia-class-component';
|
import {Pinia, Store} from 'pinia-class-component';
|
||||||
import AniListDataService from '@/data/service/AniListDataService';
|
import AniListDataService from '@/data/service/AniListDataService';
|
||||||
|
import MangaDexDataService from '@/data/service/MangaDexDataService';
|
||||||
import MangaUpdatesDataService from '@/data/service/MangaUpdatesDataService';
|
import MangaUpdatesDataService from '@/data/service/MangaUpdatesDataService';
|
||||||
|
|
||||||
@Store({
|
@Store({
|
||||||
@@ -9,6 +10,7 @@ import MangaUpdatesDataService from '@/data/service/MangaUpdatesDataService';
|
|||||||
export class ServiceStore extends Pinia {
|
export class ServiceStore extends Pinia {
|
||||||
//data
|
//data
|
||||||
private readonly intAniListDataService = new AniListDataService();
|
private readonly intAniListDataService = new AniListDataService();
|
||||||
|
private readonly intMangaDexDataService = new MangaDexDataService();
|
||||||
private readonly intMangaUpdatesDataService = new MangaUpdatesDataService();
|
private readonly intMangaUpdatesDataService = new MangaUpdatesDataService();
|
||||||
|
|
||||||
//getter
|
//getter
|
||||||
@@ -16,6 +18,10 @@ export class ServiceStore extends Pinia {
|
|||||||
return this.intAniListDataService;
|
return this.intAniListDataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get mangaDexDataService(): MangaDexDataService {
|
||||||
|
return this.intMangaDexDataService;
|
||||||
|
}
|
||||||
|
|
||||||
get mangaUpdatesDataService(): MangaUpdatesDataService {
|
get mangaUpdatesDataService(): MangaUpdatesDataService {
|
||||||
return this.intMangaUpdatesDataService;
|
return this.intMangaUpdatesDataService;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export default defineConfig({
|
|||||||
target: 'http://localhost:5000',
|
target: 'http://localhost:5000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'^/mangadex/.*$': {
|
||||||
|
target: 'http://localhost:5000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
'^/mangaupdates/.*$': {
|
'^/mangaupdates/.*$': {
|
||||||
target: 'http://localhost:5000',
|
target: 'http://localhost:5000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user