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

Commit c19ee02

Browse files
committed
feat: add kavita integration
1 parent 6510720 commit c19ee02

File tree

8 files changed

+270
-8
lines changed

8 files changed

+270
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- AlterTable
2+
ALTER TABLE "Settings" ADD COLUMN "kavitaEnabled" BOOLEAN NOT NULL DEFAULT false,
3+
ADD COLUMN "kavitaHost" TEXT,
4+
ADD COLUMN "kavitaPassword" TEXT,
5+
ADD COLUMN "kavitaUser" TEXT;

prisma/schema.prisma

+4
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,8 @@ model Settings {
7171
komgaHost String?
7272
komgaUser String?
7373
komgaPassword String?
74+
kavitaEnabled Boolean @default(false)
75+
kavitaHost String?
76+
kavitaUser String?
77+
kavitaPassword String?
7478
}

public/brand/kavita.png

10.5 KB
Loading

src/components/settings/integration.tsx

+130
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,136 @@ export function IntegrationSettings() {
171171
</Group>
172172
</Accordion.Panel>
173173
</Accordion.Item>
174+
175+
<Accordion.Item value="kavita">
176+
<Accordion.Control icon={<Image src="/brand/kavita.png" width={20} height={20} />}>Kavita</Accordion.Control>
177+
<Accordion.Panel>
178+
<Group position="apart" className={classes.item} spacing="xl" noWrap>
179+
<Box>
180+
<Breadcrumbs
181+
separator="/"
182+
styles={{
183+
separator: {
184+
marginLeft: 4,
185+
marginRight: 4,
186+
},
187+
breadcrumb: {
188+
textTransform: 'capitalize',
189+
fontSize: 13,
190+
fontWeight: 500,
191+
},
192+
root: {
193+
marginBottom: 5,
194+
},
195+
}}
196+
>
197+
Enabled
198+
</Breadcrumbs>
199+
<Text size="xs" color="dimmed">
200+
Enable Kavita integration to trigger library scan and metadata refresh tasks
201+
</Text>
202+
</Box>
203+
<SwitchItem
204+
configKey="kavitaEnabled"
205+
onUpdate={handleUpdate}
206+
initialValue={settings.data.appConfig.kavitaEnabled}
207+
/>
208+
</Group>
209+
<Group position="apart" className={classes.item} spacing="xl" noWrap>
210+
<Box>
211+
<Breadcrumbs
212+
separator="/"
213+
styles={{
214+
separator: {
215+
marginLeft: 4,
216+
marginRight: 4,
217+
},
218+
breadcrumb: {
219+
textTransform: 'capitalize',
220+
fontSize: 13,
221+
fontWeight: 500,
222+
},
223+
root: {
224+
marginBottom: 5,
225+
},
226+
}}
227+
>
228+
Host
229+
</Breadcrumbs>
230+
<Text size="xs" color="dimmed">
231+
Kavita host or ip
232+
</Text>
233+
</Box>
234+
<TextItem
235+
configKey="kavitaHost"
236+
onUpdate={handleUpdate}
237+
initialValue={settings.data.appConfig.kavitaHost}
238+
/>
239+
</Group>
240+
<Group position="apart" className={classes.item} spacing="xl" noWrap>
241+
<Box>
242+
<Breadcrumbs
243+
separator="/"
244+
styles={{
245+
separator: {
246+
marginLeft: 4,
247+
marginRight: 4,
248+
},
249+
breadcrumb: {
250+
textTransform: 'capitalize',
251+
fontSize: 13,
252+
fontWeight: 500,
253+
},
254+
root: {
255+
marginBottom: 5,
256+
},
257+
}}
258+
>
259+
Username
260+
</Breadcrumbs>
261+
<Text size="xs" color="dimmed">
262+
Kavita user
263+
</Text>
264+
</Box>
265+
<TextItem
266+
configKey="kavitaUser"
267+
onUpdate={handleUpdate}
268+
initialValue={settings.data.appConfig.kavitaUser}
269+
/>
270+
</Group>
271+
<Group position="apart" className={classes.item} spacing="xl" noWrap>
272+
<Box>
273+
<Breadcrumbs
274+
separator="/"
275+
styles={{
276+
separator: {
277+
marginLeft: 4,
278+
marginRight: 4,
279+
},
280+
breadcrumb: {
281+
textTransform: 'capitalize',
282+
fontSize: 13,
283+
fontWeight: 500,
284+
},
285+
root: {
286+
marginBottom: 5,
287+
},
288+
}}
289+
>
290+
Password
291+
</Breadcrumbs>
292+
<Text size="xs" color="dimmed">
293+
Kavita user password
294+
</Text>
295+
</Box>
296+
<TextItem
297+
configKey="kavitaPassword"
298+
onUpdate={handleUpdate}
299+
initialValue={settings.data.appConfig.kavitaPassword}
300+
/>
301+
</Group>
302+
</Accordion.Panel>
303+
</Accordion.Item>
174304
</Accordion>
175305
);
176306
}

src/server/queue/integration.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Job, Queue, Worker } from 'bullmq';
2-
import { runIntegrations } from '../utils/integration';
2+
import { scanLibrary } from '../utils/integration';
33

44
export const integrationWorker = new Worker(
55
'integrationQueue',
66
async (job: Job) => {
77
try {
8-
await runIntegrations();
8+
await scanLibrary();
99
await job.updateProgress(100);
1010
} catch (err) {
1111
await job.log(`${err}`);

src/server/utils/integration/index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as komga from './komga';
2+
import * as kavita from './kavita';
3+
4+
export const scanLibrary = async () => {
5+
await Promise.all([komga.scanLibrary(), kavita.scanLibrary()]);
6+
};
7+
8+
export const refreshMetadata = async (mangaTitle: string) => {
9+
await Promise.all([komga.refreshMetadata(mangaTitle), kavita.refreshMetadata(mangaTitle)]);
10+
};
+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { logger } from '../../../utils/logging';
2+
import { prisma } from '../../db/client';
3+
4+
interface Library {
5+
id: string;
6+
}
7+
8+
interface Series {
9+
id: string;
10+
libraryId: string;
11+
name: string;
12+
}
13+
14+
interface LoginResponse {
15+
token: string;
16+
}
17+
18+
const getToken = async (baseKavitaUrl: string, username: string, password: string) => {
19+
const kavitaLoginUrl = new URL('/api/Account/login', baseKavitaUrl).href;
20+
logger.info(`login url: ${kavitaLoginUrl}`);
21+
const response: LoginResponse = await (
22+
await fetch(kavitaLoginUrl, {
23+
method: 'POST',
24+
headers: {
25+
'Content-Type': 'application/json',
26+
Accept: 'application/json',
27+
},
28+
body: JSON.stringify({
29+
username,
30+
password,
31+
}),
32+
})
33+
).json();
34+
35+
return response.token;
36+
};
37+
38+
export const scanLibrary = async () => {
39+
const settings = await prisma.settings.findFirstOrThrow();
40+
41+
if (settings.kavitaEnabled && settings.kavitaHost && settings.kavitaUser && settings.kavitaPassword) {
42+
const baseKavitaUrl = settings.kavitaHost.toLowerCase().startsWith('http')
43+
? settings.kavitaHost
44+
: `http://${settings.kavitaHost}`;
45+
46+
const token = await getToken(baseKavitaUrl, settings.kavitaUser, settings.kavitaPassword);
47+
48+
const headers = {
49+
Authorization: `Bearer ${token}`,
50+
'Content-Type': 'application/json',
51+
Accept: 'application/json',
52+
};
53+
54+
const kavitaLibrariesUrl = new URL('/api/Library', baseKavitaUrl).href;
55+
56+
const libraries: Library[] = await (
57+
await fetch(kavitaLibrariesUrl, {
58+
headers,
59+
})
60+
).json();
61+
62+
await Promise.all(
63+
libraries.map(async (library) => {
64+
const kavitaLibraryUrl = new URL(`/api/Library/scan?libraryId=${library.id}&force=false`, baseKavitaUrl).href;
65+
await fetch(kavitaLibraryUrl, {
66+
method: 'POST',
67+
headers,
68+
});
69+
}),
70+
);
71+
}
72+
};
73+
74+
export const refreshMetadata = async (mangaName: string) => {
75+
const settings = await prisma.settings.findFirstOrThrow();
76+
77+
if (settings.kavitaEnabled && settings.kavitaHost && settings.kavitaUser && settings.kavitaPassword) {
78+
const baseKavitaUrl = settings.kavitaHost.toLowerCase().startsWith('http')
79+
? settings.kavitaHost
80+
: `http://${settings.kavitaHost}`;
81+
82+
const token = await getToken(baseKavitaUrl, settings.kavitaUser, settings.kavitaPassword);
83+
84+
const headers = {
85+
Authorization: `Bearer ${token}`,
86+
'Content-Type': 'application/json',
87+
Accept: 'application/json',
88+
};
89+
90+
const kavitaSeriesUrl = new URL('/api/Series', baseKavitaUrl).href;
91+
92+
const series: Series[] = await (
93+
await fetch(kavitaSeriesUrl, {
94+
method: 'POST',
95+
body: JSON.stringify({}),
96+
headers,
97+
})
98+
).json();
99+
100+
const content = series.find((c) => c.name === mangaName);
101+
102+
if (!content) {
103+
return;
104+
}
105+
106+
const kavitaSeriesRefreshUrl = new URL(`/api/Series/scan`, baseKavitaUrl).href;
107+
await fetch(kavitaSeriesRefreshUrl, {
108+
method: 'POST',
109+
body: JSON.stringify({
110+
libraryId: content.libraryId,
111+
seriesId: content.id,
112+
forceUpdate: true,
113+
}),
114+
headers,
115+
});
116+
}
117+
};

src/server/utils/integration.ts renamed to src/server/utils/integration/komga.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { sanitizer } from '../../utils';
2-
import { prisma } from '../db/client';
1+
import { sanitizer } from '../../../utils';
2+
import { prisma } from '../../db/client';
33

44
interface Library {
55
id: string;
@@ -75,7 +75,3 @@ export const refreshMetadata = async (mangaName: string) => {
7575
});
7676
}
7777
};
78-
79-
export const runIntegrations = async () => {
80-
await scanLibrary();
81-
};

0 commit comments

Comments
 (0)