Allow hot-reload of config.json
This commit is contained in:
parent
31026a0d15
commit
9dfc790200
10 changed files with 90 additions and 51 deletions
|
@ -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",
|
||||||
|
|
42
src/index.ts
42
src/index.ts
|
@ -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})`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
17
src/utils/get-manga-title.ts
Normal file
17
src/utils/get-manga-title.ts
Normal 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';
|
||||||
|
};
|
30
src/utils/get-manga-to-fetch.ts
Normal file
30
src/utils/get-manga-to-fetch.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,6 +0,0 @@
|
||||||
export const getUserAgent = () => {
|
|
||||||
return (
|
|
||||||
process.env.USER_AGENT ||
|
|
||||||
'Personal Chapter Update Tracker (https://git.pfy.ch/pfych/chapter-tracker)'
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue