Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Commit 04c521d

Browse files
committed
feat: add komga integration
1 parent 5cb4457 commit 04c521d

File tree

7 files changed

+133
-1
lines changed

7 files changed

+133
-1
lines changed

src/components/header.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Box, Container, createStyles, Group, Header, Title, UnstyledButton } fr
22
import Image from 'next/image';
33
import Link from 'next/link';
44
import { SearchControl } from './headerSearch';
5+
import { SettingsMenuButton } from './settingsMenu';
56

67
const useStyles = createStyles((theme) => ({
78
header: {
@@ -44,8 +45,9 @@ export function KaizokuHeader() {
4445
</UnstyledButton>
4546
</Link>
4647

47-
<Group spacing={5}>
48+
<Group position="center" spacing={5}>
4849
<SearchControl />
50+
<SettingsMenuButton />
4951
</Group>
5052
</Box>
5153
</Container>

src/server/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import next from 'next';
66
import { logger } from '../utils/logging';
77
import { checkChaptersQueue, scheduleAll } from './queue/checkChapters';
88
import { downloadQueue } from './queue/download';
9+
import { integrationQueue } from './queue/integration';
910
import { notificationQueue } from './queue/notify';
1011
import { updateMetadataQueue } from './queue/updateMetadata';
1112

@@ -23,6 +24,7 @@ createBullBoard({
2324
new BullAdapter(checkChaptersQueue),
2425
new BullAdapter(notificationQueue),
2526
new BullAdapter(updateMetadataQueue),
27+
new BullAdapter(integrationQueue),
2628
],
2729
serverAdapter,
2830
});

src/server/queue/download.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { sanitizer } from '../../utils';
44
import { logger } from '../../utils/logging';
55
import { prisma } from '../db/client';
66
import { downloadChapter, getChapterFromLocal, getMangaPath, removeManga } from '../utils/mangal';
7+
import { integrationQueue } from './integration';
78
import { notificationQueue } from './notify';
89

910
const mangaWithLibraryAndMetadata = Prisma.validator<Prisma.MangaArgs>()({
@@ -61,6 +62,7 @@ export const downloadWorker = new Worker(
6162
source: manga.source,
6263
url: manga.metadata.urls.find((url) => url.includes('anilist')),
6364
});
65+
await integrationQueue.add('run_integrations', null);
6466
await job.updateProgress(100);
6567
} catch (err) {
6668
await job.log(`${err}`);

src/server/queue/integration.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Job, Queue, Worker } from 'bullmq';
2+
import { runIntegrations } from '../utils/integration';
3+
4+
export const integrationWorker = new Worker(
5+
'integrationQueue',
6+
async (job: Job) => {
7+
try {
8+
await runIntegrations();
9+
await job.updateProgress(100);
10+
} catch (err) {
11+
await job.log(`${err}`);
12+
throw err;
13+
}
14+
},
15+
{
16+
connection: {
17+
host: process.env.REDIS_HOST,
18+
port: parseInt(process.env.REDIS_PORT || '6379', 10),
19+
},
20+
concurrency: 30,
21+
limiter: {
22+
max: 30,
23+
duration: 1000 * 2,
24+
},
25+
},
26+
);
27+
28+
export const integrationQueue = new Queue('integrationQueue', {
29+
connection: {
30+
host: process.env.REDIS_HOST,
31+
port: parseInt(process.env.REDIS_PORT || '6379', 10),
32+
},
33+
defaultJobOptions: {
34+
removeOnComplete: true,
35+
attempts: 20,
36+
backoff: {
37+
type: 'fixed',
38+
delay: 1000 * 60 * 2,
39+
},
40+
},
41+
});

src/server/queue/updateMetadata.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Job, Queue, Worker } from 'bullmq';
2+
import { refreshMetadata } from '../utils/integration';
23

34
import { getMangaPath, updateExistingMangaMetadata } from '../utils/mangal';
45

@@ -13,6 +14,7 @@ export const updateMetadataWorker = new Worker(
1314
const { libraryPath, mangaTitle }: IUpdateMetadataWorkerData = job.data;
1415
try {
1516
await updateExistingMangaMetadata(libraryPath, mangaTitle);
17+
await refreshMetadata(mangaTitle);
1618
await job.updateProgress(100);
1719
} catch (err) {
1820
await job.log(`${err}`);

src/server/trpc/router/manga.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { isCronValid, sanitizer } from '../../../utils';
55
import { checkChaptersQueue, removeJob, schedule } from '../../queue/checkChapters';
66
import { downloadQueue, downloadWorker, removeDownloadJobs } from '../../queue/download';
77
import { scheduleUpdateMetadata } from '../../queue/updateMetadata';
8+
import { scanLibrary } from '../../utils/integration';
89
import { bindTitleToAnilistId, getAvailableSources, getMangaDetail, removeManga, search } from '../../utils/mangal';
910
import { t } from '../trpc';
1011

@@ -106,6 +107,7 @@ export const mangaRouter = t.router({
106107
if (shouldRemoveFiles === true) {
107108
const mangaPath = path.resolve(removed.library.path, sanitizer(removed.title));
108109
await removeManga(mangaPath);
110+
await scanLibrary();
109111
}
110112
downloadWorker.resume();
111113
}),

src/server/utils/integration.ts

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { sanitizer } from '../../utils';
2+
import { prisma } from '../db/client';
3+
4+
interface Library {
5+
id: string;
6+
}
7+
8+
interface Series {
9+
content: SeriesContent[];
10+
}
11+
12+
interface SeriesContent {
13+
id: string;
14+
name: string;
15+
}
16+
17+
export const scanLibrary = async () => {
18+
const settings = await prisma.settings.findFirstOrThrow();
19+
20+
if (settings.komgaEnabled && settings.komgaHost && settings.komgaUser && settings.komgaPassword) {
21+
const baseKomgaUrl = settings.komgaHost.toLowerCase().startsWith('http')
22+
? settings.komgaHost
23+
: `http://${settings.komgaHost}`;
24+
const headers = {
25+
Authorization: `Basic ${Buffer.from(`${settings.komgaUser}:${settings.komgaPassword}`).toString('base64')}`,
26+
};
27+
const komgaLibrariesUrl = new URL('/api/v1/libraries', baseKomgaUrl).href;
28+
29+
const libraries: Library[] = await (
30+
await fetch(komgaLibrariesUrl, {
31+
headers,
32+
})
33+
).json();
34+
35+
await Promise.all(
36+
libraries.map(async (library) => {
37+
const komgaLibraryUrl = new URL(`/api/v1/libraries/${library.id}/scan`, baseKomgaUrl).href;
38+
await fetch(komgaLibraryUrl, {
39+
method: 'POST',
40+
headers,
41+
});
42+
}),
43+
);
44+
}
45+
};
46+
47+
export const refreshMetadata = async (mangaName: string) => {
48+
const settings = await prisma.settings.findFirstOrThrow();
49+
50+
if (settings.komgaEnabled && settings.komgaHost && settings.komgaUser && settings.komgaPassword) {
51+
const baseKomgaUrl = settings.komgaHost.toLowerCase().startsWith('http')
52+
? settings.komgaHost
53+
: `http://${settings.komgaHost}`;
54+
const headers = {
55+
Authorization: `Basic ${Buffer.from(`${settings.komgaUser}:${settings.komgaPassword}`).toString('base64')}`,
56+
};
57+
const komgaSeriesUrl = new URL('/api/v1/series?size=1000', baseKomgaUrl).href;
58+
59+
const series: Series = await (
60+
await fetch(komgaSeriesUrl, {
61+
headers,
62+
})
63+
).json();
64+
65+
const content = series.content.find((c) => c.name === sanitizer(mangaName));
66+
67+
if (!content) {
68+
return;
69+
}
70+
71+
const komgaSeriesRefreshUrl = new URL(`/api/v1/series/${content.id}/metadata/refresh`, baseKomgaUrl).href;
72+
await fetch(komgaSeriesRefreshUrl, {
73+
method: 'POST',
74+
headers,
75+
});
76+
}
77+
};
78+
79+
export const runIntegrations = async () => {
80+
await scanLibrary();
81+
};

0 commit comments

Comments
 (0)