Skip to content

Commit

Permalink
feat: add kavita integration
Browse files Browse the repository at this point in the history
  • Loading branch information
oae committed Nov 24, 2022
1 parent 6510720 commit c19ee02
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 8 deletions.
5 changes: 5 additions & 0 deletions prisma/migrations/20221124200902_add_kavita/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "kavitaEnabled" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "kavitaHost" TEXT,
ADD COLUMN "kavitaPassword" TEXT,
ADD COLUMN "kavitaUser" TEXT;
4 changes: 4 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,8 @@ model Settings {
komgaHost String?
komgaUser String?
komgaPassword String?
kavitaEnabled Boolean @default(false)
kavitaHost String?
kavitaUser String?
kavitaPassword String?
}
Binary file added public/brand/kavita.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
130 changes: 130 additions & 0 deletions src/components/settings/integration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,136 @@ export function IntegrationSettings() {
</Group>
</Accordion.Panel>
</Accordion.Item>

<Accordion.Item value="kavita">
<Accordion.Control icon={<Image src="/brand/kavita.png" width={20} height={20} />}>Kavita</Accordion.Control>
<Accordion.Panel>
<Group position="apart" className={classes.item} spacing="xl" noWrap>
<Box>
<Breadcrumbs
separator="/"
styles={{
separator: {
marginLeft: 4,
marginRight: 4,
},
breadcrumb: {
textTransform: 'capitalize',
fontSize: 13,
fontWeight: 500,
},
root: {
marginBottom: 5,
},
}}
>
Enabled
</Breadcrumbs>
<Text size="xs" color="dimmed">
Enable Kavita integration to trigger library scan and metadata refresh tasks
</Text>
</Box>
<SwitchItem
configKey="kavitaEnabled"
onUpdate={handleUpdate}
initialValue={settings.data.appConfig.kavitaEnabled}
/>
</Group>
<Group position="apart" className={classes.item} spacing="xl" noWrap>
<Box>
<Breadcrumbs
separator="/"
styles={{
separator: {
marginLeft: 4,
marginRight: 4,
},
breadcrumb: {
textTransform: 'capitalize',
fontSize: 13,
fontWeight: 500,
},
root: {
marginBottom: 5,
},
}}
>
Host
</Breadcrumbs>
<Text size="xs" color="dimmed">
Kavita host or ip
</Text>
</Box>
<TextItem
configKey="kavitaHost"
onUpdate={handleUpdate}
initialValue={settings.data.appConfig.kavitaHost}
/>
</Group>
<Group position="apart" className={classes.item} spacing="xl" noWrap>
<Box>
<Breadcrumbs
separator="/"
styles={{
separator: {
marginLeft: 4,
marginRight: 4,
},
breadcrumb: {
textTransform: 'capitalize',
fontSize: 13,
fontWeight: 500,
},
root: {
marginBottom: 5,
},
}}
>
Username
</Breadcrumbs>
<Text size="xs" color="dimmed">
Kavita user
</Text>
</Box>
<TextItem
configKey="kavitaUser"
onUpdate={handleUpdate}
initialValue={settings.data.appConfig.kavitaUser}
/>
</Group>
<Group position="apart" className={classes.item} spacing="xl" noWrap>
<Box>
<Breadcrumbs
separator="/"
styles={{
separator: {
marginLeft: 4,
marginRight: 4,
},
breadcrumb: {
textTransform: 'capitalize',
fontSize: 13,
fontWeight: 500,
},
root: {
marginBottom: 5,
},
}}
>
Password
</Breadcrumbs>
<Text size="xs" color="dimmed">
Kavita user password
</Text>
</Box>
<TextItem
configKey="kavitaPassword"
onUpdate={handleUpdate}
initialValue={settings.data.appConfig.kavitaPassword}
/>
</Group>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
}
4 changes: 2 additions & 2 deletions src/server/queue/integration.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Job, Queue, Worker } from 'bullmq';
import { runIntegrations } from '../utils/integration';
import { scanLibrary } from '../utils/integration';

export const integrationWorker = new Worker(
'integrationQueue',
async (job: Job) => {
try {
await runIntegrations();
await scanLibrary();
await job.updateProgress(100);
} catch (err) {
await job.log(`${err}`);
Expand Down
10 changes: 10 additions & 0 deletions src/server/utils/integration/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as komga from './komga';
import * as kavita from './kavita';

export const scanLibrary = async () => {
await Promise.all([komga.scanLibrary(), kavita.scanLibrary()]);
};

export const refreshMetadata = async (mangaTitle: string) => {
await Promise.all([komga.refreshMetadata(mangaTitle), kavita.refreshMetadata(mangaTitle)]);
};
117 changes: 117 additions & 0 deletions src/server/utils/integration/kavita.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { logger } from '../../../utils/logging';
import { prisma } from '../../db/client';

interface Library {
id: string;
}

interface Series {
id: string;
libraryId: string;
name: string;
}

interface LoginResponse {
token: string;
}

const getToken = async (baseKavitaUrl: string, username: string, password: string) => {
const kavitaLoginUrl = new URL('/api/Account/login', baseKavitaUrl).href;
logger.info(`login url: ${kavitaLoginUrl}`);
const response: LoginResponse = await (
await fetch(kavitaLoginUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
username,
password,
}),
})
).json();

return response.token;
};

export const scanLibrary = async () => {
const settings = await prisma.settings.findFirstOrThrow();

if (settings.kavitaEnabled && settings.kavitaHost && settings.kavitaUser && settings.kavitaPassword) {
const baseKavitaUrl = settings.kavitaHost.toLowerCase().startsWith('http')
? settings.kavitaHost
: `http://${settings.kavitaHost}`;

const token = await getToken(baseKavitaUrl, settings.kavitaUser, settings.kavitaPassword);

const headers = {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
Accept: 'application/json',
};

const kavitaLibrariesUrl = new URL('/api/Library', baseKavitaUrl).href;

const libraries: Library[] = await (
await fetch(kavitaLibrariesUrl, {
headers,
})
).json();

await Promise.all(
libraries.map(async (library) => {
const kavitaLibraryUrl = new URL(`/api/Library/scan?libraryId=${library.id}&force=false`, baseKavitaUrl).href;
await fetch(kavitaLibraryUrl, {
method: 'POST',
headers,
});
}),
);
}
};

export const refreshMetadata = async (mangaName: string) => {
const settings = await prisma.settings.findFirstOrThrow();

if (settings.kavitaEnabled && settings.kavitaHost && settings.kavitaUser && settings.kavitaPassword) {
const baseKavitaUrl = settings.kavitaHost.toLowerCase().startsWith('http')
? settings.kavitaHost
: `http://${settings.kavitaHost}`;

const token = await getToken(baseKavitaUrl, settings.kavitaUser, settings.kavitaPassword);

const headers = {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
Accept: 'application/json',
};

const kavitaSeriesUrl = new URL('/api/Series', baseKavitaUrl).href;

const series: Series[] = await (
await fetch(kavitaSeriesUrl, {
method: 'POST',
body: JSON.stringify({}),
headers,
})
).json();

const content = series.find((c) => c.name === mangaName);

if (!content) {
return;
}

const kavitaSeriesRefreshUrl = new URL(`/api/Series/scan`, baseKavitaUrl).href;
await fetch(kavitaSeriesRefreshUrl, {
method: 'POST',
body: JSON.stringify({
libraryId: content.libraryId,
seriesId: content.id,
forceUpdate: true,
}),
headers,
});
}
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { sanitizer } from '../../utils';
import { prisma } from '../db/client';
import { sanitizer } from '../../../utils';
import { prisma } from '../../db/client';

interface Library {
id: string;
Expand Down Expand Up @@ -75,7 +75,3 @@ export const refreshMetadata = async (mangaName: string) => {
});
}
};

export const runIntegrations = async () => {
await scanLibrary();
};

0 comments on commit c19ee02

Please sign in to comment.