Allow hot-reload of config.json

This commit is contained in:
pfych 2025-04-20 15:30:05 +10:00
parent 31026a0d15
commit 9dfc790200
10 changed files with 90 additions and 51 deletions

View file

@ -21,9 +21,12 @@ https://mangadex.org/title/<manga-id>/<manga-name>
The Check Interval is how often the script will check for updates, in seconds. The Check Interval is how often the script will check for updates, in seconds.
MangaDex requires setting a `userAgent` when making requests to their API, Please set this to something unique to you.
```json ```json
{ {
"checkInterval": 900, "checkInterval": 900,
"userAgent": "https://git.pfy.ch/pfych/chapter-tracker (Run by your-email@example.com)",
"mangaByWebhook": { "mangaByWebhook": {
"https://discord.com/api/webhooks/id/token": [ "https://discord.com/api/webhooks/id/token": [
"9d3d3403-1a87-4737-9803-bc3d99db1424", "9d3d3403-1a87-4737-9803-bc3d99db1424",

View file

@ -3,46 +3,35 @@ import { getManga } from './mangadex/manga';
import { getCover } from './mangadex/cover'; import { getCover } from './mangadex/cover';
import { lazyKv } from './db/lazyKv'; import { lazyKv } from './db/lazyKv';
import { sendWebhook } from './utils/webhook'; import { sendWebhook } from './utils/webhook';
import { flatten, uniq } from 'lodash-es'; import { ChapterId } from './types';
import { ChapterId, MangaByWebhook } from './types'; import { getMangaToFetch } from './utils/get-manga-to-fetch';
import { getMangaTitle } from './utils/get-manga-title';
const config = new lazyKv('./config.json'); const config = new lazyKv('./config.json');
const mangaHistory = new lazyKv('./mangaHistory.json'); const mangaHistory = new lazyKv('./mangaHistory.json');
void (async () => { void (async () => {
const checkInterval = await config.get<number>('checkInterval', 900); const checkInterval = await config.get<number>('checkInterval', 900);
const mangaByWebhook = await config.get<MangaByWebhook>('mangaByWebhook', {}); const userAgent = await config.get<string>(
'userAgent',
const uniqueMangaIds = uniq(flatten(Object.values(mangaByWebhook))); 'https://git.pfy.ch/pfych/chapter-tracker (Missing custom userAgent? User Miss-configured!)',
const mangaIdsToWebhooks = uniqueMangaIds.reduce((acc, mangaId) => {
return {
...acc,
[mangaId]: Object.keys(mangaByWebhook).filter((webhookUrl) =>
mangaByWebhook[webhookUrl].includes(mangaId),
),
};
}, {} as MangaByWebhook);
console.log(
`Config loaded (${uniqueMangaIds.length} Manga, ${Object.keys(mangaByWebhook).length} Webhooks) `,
); );
const checkForUpdates = async () => { const checkForUpdates = async () => {
console.log('\nChecking for updates...'); const { uniqueMangaIds, mangaIdsToWebhooks } =
await getMangaToFetch(config);
for (const mangaId of uniqueMangaIds) { for (const mangaId of uniqueMangaIds) {
const lastChapterId = await mangaHistory.get<ChapterId>(mangaId); const lastChapterId = await mangaHistory.get<ChapterId>(mangaId);
const manga = await getManga(mangaId); const manga = await getManga(mangaId, userAgent);
const chapters = await getAllChapters(mangaId); const chapters = await getAllChapters(mangaId, userAgent);
const latestChapter = getLatestChapter(chapters); const latestChapter = getLatestChapter(chapters);
const cover = await getCover(manga); const cover = await getCover(manga, userAgent);
const title = getMangaTitle(manga);
if (lastChapterId !== latestChapter.id) { if (lastChapterId !== latestChapter.id) {
console.log( console.log('Update found for manga:', title);
'Update found for manga:',
manga.attributes.title.en || `MISSING ENGLISH TITLE (${mangaId})`,
);
const webhooksForManga = mangaIdsToWebhooks[mangaId]; const webhooksForManga = mangaIdsToWebhooks[mangaId];
@ -60,10 +49,7 @@ void (async () => {
await mangaHistory.set(mangaId, latestChapter.id); await mangaHistory.set(mangaId, latestChapter.id);
} else { } else {
console.log( console.log('No Updates found for manga:', title);
'No Updates found for manga:',
manga.attributes.title.en || `MISSING ENGLISH TITLE (${mangaId})`,
);
} }
} }
}; };

View file

@ -1,9 +1,11 @@
import axios from 'axios'; import axios from 'axios';
import { sleep } from '../utils/sleep'; import { sleep } from '../utils/sleep';
import { Chapter } from '../types'; import { Chapter } from '../types';
import { getUserAgent } from '../utils/user-agent';
export const getAllChapters = async (id: string): Promise<Chapter[]> => { export const getAllChapters = async (
id: string,
userAgent: string,
): Promise<Chapter[]> => {
let nextPage = false; let nextPage = false;
let offset = 0; let offset = 0;
@ -20,7 +22,7 @@ export const getAllChapters = async (id: string): Promise<Chapter[]> => {
offset, offset,
}, },
headers: { headers: {
'User-Agent': getUserAgent(), 'User-Agent': userAgent,
}, },
}); });

View file

@ -1,8 +1,10 @@
import axios from 'axios'; import axios from 'axios';
import { Manga } from '../types'; import { Manga } from '../types';
import { getUserAgent } from '../utils/user-agent';
export const getCover = async (manga: Manga): Promise<string> => { export const getCover = async (
manga: Manga,
userAgent: string,
): Promise<string> => {
const coverId = Object.values(manga.relationships).find( const coverId = Object.values(manga.relationships).find(
(relationship) => relationship.type === 'cover_art', (relationship) => relationship.type === 'cover_art',
)?.id; )?.id;
@ -15,7 +17,7 @@ export const getCover = async (manga: Manga): Promise<string> => {
data: { attributes: { fileName: string } }; data: { attributes: { fileName: string } };
}>(`https://api.mangadex.org/cover/${coverId}`, { }>(`https://api.mangadex.org/cover/${coverId}`, {
headers: { headers: {
'User-Agent': getUserAgent(), 'User-Agent': userAgent,
}, },
}); });

View file

@ -1,8 +1,10 @@
import axios from 'axios'; import axios from 'axios';
import { Manga } from '../types'; import { Manga } from '../types';
import { getUserAgent } from '../utils/user-agent';
export const getManga = async (id: string): Promise<Manga> => { export const getManga = async (
id: string,
userAgent: string,
): Promise<Manga> => {
const response = await axios.get<{ const response = await axios.get<{
data: Manga; data: Manga;
limit: number; limit: number;
@ -10,7 +12,7 @@ export const getManga = async (id: string): Promise<Manga> => {
total: number; total: number;
}>(`https://api.mangadex.org/manga/${id}`, { }>(`https://api.mangadex.org/manga/${id}`, {
headers: { headers: {
'User-Agent': getUserAgent(), 'User-Agent': userAgent,
}, },
}); });

View file

@ -1,9 +1,10 @@
type MangaId = string; export type MangaId = string;
type ChapterId = string; export type ChapterId = string;
type WebhookUrl = string; export type WebhookUrl = string;
type MangaByWebhook = Record<WebhookUrl, MangaId[]>; export type MangaByWebhook = Record<WebhookUrl, MangaId[]>;
type RelationshipType = 'author' | 'artist' | 'cover_art' | 'creator'; export type RelationshipType = 'author' | 'artist' | 'cover_art' | 'creator';
type Language = 'en' | 'ja' | 'zh'; export const languages = ['en', 'ja-ro', 'ja', 'zh-ro', 'zh'] as const; // Preferred title based off order of this array!
export type Language = (typeof languages)[number];
export interface Chapter { export interface Chapter {
id: ChapterId; id: ChapterId;

View file

@ -0,0 +1,17 @@
import { languages, Manga } from '../types';
export const getMangaTitle = (manga: Manga) => {
const languageToUse = languages.find((lang) => manga.attributes.title[lang]);
if (languageToUse) {
return manga.attributes.title[languageToUse];
}
// If no language is found, use the first one returned by the API
const fallbackLanguage = Object.keys(manga.attributes.title)?.[0];
if (fallbackLanguage) {
return manga.attributes.title[fallbackLanguage] as string;
}
return 'UNKNOWN TITLE';
};

View file

@ -0,0 +1,30 @@
import { lazyKv } from '../db/lazyKv';
import { MangaByWebhook, MangaId } from '../types';
import { flatten, uniq } from 'lodash-es';
export const getMangaToFetch = async (
config: lazyKv,
): Promise<{
uniqueMangaIds: MangaId[];
mangaIdsToWebhooks: MangaByWebhook;
}> => {
const mangaByWebhook = await config.get<MangaByWebhook>('mangaByWebhook', {});
const uniqueMangaIds = uniq(flatten(Object.values(mangaByWebhook)));
const mangaIdsToWebhooks = uniqueMangaIds.reduce((acc, mangaId) => {
return {
...acc,
[mangaId]: Object.keys(mangaByWebhook).filter((webhookUrl) =>
mangaByWebhook[webhookUrl].includes(mangaId),
),
};
}, {} as MangaByWebhook);
console.log(
`Config loaded (${uniqueMangaIds.length} Manga, ${Object.keys(mangaByWebhook).length} Webhooks) `,
);
return {
uniqueMangaIds,
mangaIdsToWebhooks,
};
};

View file

@ -1,6 +0,0 @@
export const getUserAgent = () => {
return (
process.env.USER_AGENT ||
'Personal Chapter Update Tracker (https://git.pfy.ch/pfych/chapter-tracker)'
);
};

View file

@ -1,6 +1,7 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import axios from 'axios'; import axios from 'axios';
import { Chapter, Manga } from '../types'; import { Chapter, Manga } from '../types';
import { getMangaTitle } from './get-manga-title';
export const sendWebhook = async (args: { export const sendWebhook = async (args: {
webhookUrl: string; webhookUrl: string;
@ -9,16 +10,17 @@ export const sendWebhook = async (args: {
cover: string; cover: string;
}) => { }) => {
const { webhookUrl, manga, latestChapter, cover } = args; const { webhookUrl, manga, latestChapter, cover } = args;
const title = getMangaTitle(manga);
await axios.post(webhookUrl, { await axios.post(webhookUrl, {
username: 'Manga Updates', username: 'Manga Updates',
avatar_url: 'https://assets.pfy.ch/icons/manga.png', avatar_url: 'https://assets.pfy.ch/icons/manga.png',
content: `[New chapter for ${manga.attributes.title.en || 'MISSING ENGLISH TITLE'}](https://mangadex.org/chapter/${latestChapter.id})`, content: `[New chapter for ${title}](https://mangadex.org/chapter/${latestChapter.id})`,
embeds: [ embeds: [
{ {
author: { author: {
icon_url: 'https://assets.pfy.ch/icons/manga.png', icon_url: 'https://assets.pfy.ch/icons/manga.png',
name: manga.attributes.title.en || 'MISSING ENGLISH TITLE', name: title,
}, },
thumbnail: { thumbnail: {
url: cover, url: cover,