From 04c521de5b4b429172990fe45c579a3883fdc752 Mon Sep 17 00:00:00 2001 From: Alperen Elhan Date: Fri, 4 Nov 2022 02:58:27 +0300 Subject: [PATCH] feat: add komga integration --- src/components/header.tsx | 4 +- src/server/index.ts | 2 + src/server/queue/download.ts | 2 + src/server/queue/integration.ts | 41 +++++++++++++++ src/server/queue/updateMetadata.ts | 2 + src/server/trpc/router/manga.ts | 2 + src/server/utils/integration.ts | 81 ++++++++++++++++++++++++++++++ 7 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/server/queue/integration.ts create mode 100644 src/server/utils/integration.ts diff --git a/src/components/header.tsx b/src/components/header.tsx index 2afe357..9280d72 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -2,6 +2,7 @@ import { Box, Container, createStyles, Group, Header, Title, UnstyledButton } fr import Image from 'next/image'; import Link from 'next/link'; import { SearchControl } from './headerSearch'; +import { SettingsMenuButton } from './settingsMenu'; const useStyles = createStyles((theme) => ({ header: { @@ -44,8 +45,9 @@ export function KaizokuHeader() { - + + diff --git a/src/server/index.ts b/src/server/index.ts index 464e5a9..84d1256 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -6,6 +6,7 @@ import next from 'next'; import { logger } from '../utils/logging'; import { checkChaptersQueue, scheduleAll } from './queue/checkChapters'; import { downloadQueue } from './queue/download'; +import { integrationQueue } from './queue/integration'; import { notificationQueue } from './queue/notify'; import { updateMetadataQueue } from './queue/updateMetadata'; @@ -23,6 +24,7 @@ createBullBoard({ new BullAdapter(checkChaptersQueue), new BullAdapter(notificationQueue), new BullAdapter(updateMetadataQueue), + new BullAdapter(integrationQueue), ], serverAdapter, }); diff --git a/src/server/queue/download.ts b/src/server/queue/download.ts index d5825c2..713944f 100644 --- a/src/server/queue/download.ts +++ b/src/server/queue/download.ts @@ -4,6 +4,7 @@ import { sanitizer } from '../../utils'; import { logger } from '../../utils/logging'; import { prisma } from '../db/client'; import { downloadChapter, getChapterFromLocal, getMangaPath, removeManga } from '../utils/mangal'; +import { integrationQueue } from './integration'; import { notificationQueue } from './notify'; const mangaWithLibraryAndMetadata = Prisma.validator()({ @@ -61,6 +62,7 @@ export const downloadWorker = new Worker( source: manga.source, url: manga.metadata.urls.find((url) => url.includes('anilist')), }); + await integrationQueue.add('run_integrations', null); await job.updateProgress(100); } catch (err) { await job.log(`${err}`); diff --git a/src/server/queue/integration.ts b/src/server/queue/integration.ts new file mode 100644 index 0000000..b1fe55d --- /dev/null +++ b/src/server/queue/integration.ts @@ -0,0 +1,41 @@ +import { Job, Queue, Worker } from 'bullmq'; +import { runIntegrations } from '../utils/integration'; + +export const integrationWorker = new Worker( + 'integrationQueue', + async (job: Job) => { + try { + await runIntegrations(); + await job.updateProgress(100); + } catch (err) { + await job.log(`${err}`); + throw err; + } + }, + { + connection: { + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT || '6379', 10), + }, + concurrency: 30, + limiter: { + max: 30, + duration: 1000 * 2, + }, + }, +); + +export const integrationQueue = new Queue('integrationQueue', { + connection: { + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT || '6379', 10), + }, + defaultJobOptions: { + removeOnComplete: true, + attempts: 20, + backoff: { + type: 'fixed', + delay: 1000 * 60 * 2, + }, + }, +}); diff --git a/src/server/queue/updateMetadata.ts b/src/server/queue/updateMetadata.ts index 29c100e..b52d7ae 100644 --- a/src/server/queue/updateMetadata.ts +++ b/src/server/queue/updateMetadata.ts @@ -1,4 +1,5 @@ import { Job, Queue, Worker } from 'bullmq'; +import { refreshMetadata } from '../utils/integration'; import { getMangaPath, updateExistingMangaMetadata } from '../utils/mangal'; @@ -13,6 +14,7 @@ export const updateMetadataWorker = new Worker( const { libraryPath, mangaTitle }: IUpdateMetadataWorkerData = job.data; try { await updateExistingMangaMetadata(libraryPath, mangaTitle); + await refreshMetadata(mangaTitle); await job.updateProgress(100); } catch (err) { await job.log(`${err}`); diff --git a/src/server/trpc/router/manga.ts b/src/server/trpc/router/manga.ts index 753c37e..7213f4b 100644 --- a/src/server/trpc/router/manga.ts +++ b/src/server/trpc/router/manga.ts @@ -5,6 +5,7 @@ import { isCronValid, sanitizer } from '../../../utils'; import { checkChaptersQueue, removeJob, schedule } from '../../queue/checkChapters'; import { downloadQueue, downloadWorker, removeDownloadJobs } from '../../queue/download'; import { scheduleUpdateMetadata } from '../../queue/updateMetadata'; +import { scanLibrary } from '../../utils/integration'; import { bindTitleToAnilistId, getAvailableSources, getMangaDetail, removeManga, search } from '../../utils/mangal'; import { t } from '../trpc'; @@ -106,6 +107,7 @@ export const mangaRouter = t.router({ if (shouldRemoveFiles === true) { const mangaPath = path.resolve(removed.library.path, sanitizer(removed.title)); await removeManga(mangaPath); + await scanLibrary(); } downloadWorker.resume(); }), diff --git a/src/server/utils/integration.ts b/src/server/utils/integration.ts new file mode 100644 index 0000000..fa55621 --- /dev/null +++ b/src/server/utils/integration.ts @@ -0,0 +1,81 @@ +import { sanitizer } from '../../utils'; +import { prisma } from '../db/client'; + +interface Library { + id: string; +} + +interface Series { + content: SeriesContent[]; +} + +interface SeriesContent { + id: string; + name: string; +} + +export const scanLibrary = async () => { + const settings = await prisma.settings.findFirstOrThrow(); + + if (settings.komgaEnabled && settings.komgaHost && settings.komgaUser && settings.komgaPassword) { + const baseKomgaUrl = settings.komgaHost.toLowerCase().startsWith('http') + ? settings.komgaHost + : `http://${settings.komgaHost}`; + const headers = { + Authorization: `Basic ${Buffer.from(`${settings.komgaUser}:${settings.komgaPassword}`).toString('base64')}`, + }; + const komgaLibrariesUrl = new URL('/api/v1/libraries', baseKomgaUrl).href; + + const libraries: Library[] = await ( + await fetch(komgaLibrariesUrl, { + headers, + }) + ).json(); + + await Promise.all( + libraries.map(async (library) => { + const komgaLibraryUrl = new URL(`/api/v1/libraries/${library.id}/scan`, baseKomgaUrl).href; + await fetch(komgaLibraryUrl, { + method: 'POST', + headers, + }); + }), + ); + } +}; + +export const refreshMetadata = async (mangaName: string) => { + const settings = await prisma.settings.findFirstOrThrow(); + + if (settings.komgaEnabled && settings.komgaHost && settings.komgaUser && settings.komgaPassword) { + const baseKomgaUrl = settings.komgaHost.toLowerCase().startsWith('http') + ? settings.komgaHost + : `http://${settings.komgaHost}`; + const headers = { + Authorization: `Basic ${Buffer.from(`${settings.komgaUser}:${settings.komgaPassword}`).toString('base64')}`, + }; + const komgaSeriesUrl = new URL('/api/v1/series?size=1000', baseKomgaUrl).href; + + const series: Series = await ( + await fetch(komgaSeriesUrl, { + headers, + }) + ).json(); + + const content = series.content.find((c) => c.name === sanitizer(mangaName)); + + if (!content) { + return; + } + + const komgaSeriesRefreshUrl = new URL(`/api/v1/series/${content.id}/metadata/refresh`, baseKomgaUrl).href; + await fetch(komgaSeriesRefreshUrl, { + method: 'POST', + headers, + }); + } +}; + +export const runIntegrations = async () => { + await scanLibrary(); +};