diff --git a/src/server/index.ts b/src/server/index.ts
index 3d617c4..464e5a9 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -7,6 +7,7 @@ import { logger } from '../utils/logging';
import { checkChaptersQueue, scheduleAll } from './queue/checkChapters';
import { downloadQueue } from './queue/download';
import { notificationQueue } from './queue/notify';
+import { updateMetadataQueue } from './queue/updateMetadata';
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
@@ -17,7 +18,12 @@ const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath('/bull/queues');
createBullBoard({
- queues: [new BullAdapter(downloadQueue), new BullAdapter(checkChaptersQueue), new BullAdapter(notificationQueue)],
+ queues: [
+ new BullAdapter(downloadQueue),
+ new BullAdapter(checkChaptersQueue),
+ new BullAdapter(notificationQueue),
+ new BullAdapter(updateMetadataQueue),
+ ],
serverAdapter,
});
diff --git a/src/server/queue/updateMetadata.ts b/src/server/queue/updateMetadata.ts
new file mode 100644
index 0000000..b12fa72
--- /dev/null
+++ b/src/server/queue/updateMetadata.ts
@@ -0,0 +1,57 @@
+import { Job, Queue, Worker } from 'bullmq';
+
+import { getMangaPath, updateExistingMangaMetadata } from '../utils/mangal';
+
+export interface IUpdateMetadataWorkerData {
+ libraryPath: string;
+ mangaTitle: string;
+}
+
+export const updateMetadataWorker = new Worker(
+ 'updateMetadataQueue',
+ async (job: Job) => {
+ const { libraryPath, mangaTitle }: IUpdateMetadataWorkerData = job.data;
+ try {
+ await updateExistingMangaMetadata(libraryPath, mangaTitle);
+ 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: 5,
+ },
+);
+
+export const updateMetadataQueue = new Queue('updateMetadataQueue', {
+ connection: {
+ host: process.env.REDIS_HOST,
+ port: parseInt(process.env.REDIS_PORT || '6379', 10),
+ },
+ defaultJobOptions: {
+ removeOnComplete: true,
+ attempts: 10,
+ backoff: {
+ type: 'fixed',
+ delay: 1000 * 60 * 2,
+ },
+ },
+});
+
+export const scheduleUpdateMetadata = async (libraryPath: string, mangaTitle: string) => {
+ await updateMetadataQueue.add(
+ getMangaPath(libraryPath, mangaTitle),
+ {
+ libraryPath,
+ mangaTitle,
+ },
+ {
+ jobId: getMangaPath(libraryPath, mangaTitle),
+ },
+ );
+};
diff --git a/src/server/trpc/router/manga.ts b/src/server/trpc/router/manga.ts
index e5c128f..753c37e 100644
--- a/src/server/trpc/router/manga.ts
+++ b/src/server/trpc/router/manga.ts
@@ -4,14 +4,8 @@ import { z } from 'zod';
import { isCronValid, sanitizer } from '../../../utils';
import { checkChaptersQueue, removeJob, schedule } from '../../queue/checkChapters';
import { downloadQueue, downloadWorker, removeDownloadJobs } from '../../queue/download';
-import {
- bindTitleToAnilistId,
- getAvailableSources,
- getMangaDetail,
- Manga,
- removeManga,
- search,
-} from '../../utils/mangal';
+import { scheduleUpdateMetadata } from '../../queue/updateMetadata';
+import { bindTitleToAnilistId, getAvailableSources, getMangaDetail, removeManga, search } from '../../utils/mangal';
import { t } from '../trpc';
export const mangaRouter = t.router({
@@ -73,12 +67,15 @@ export const mangaRouter = t.router({
)
.query(async ({ input }) => {
const { keyword, source } = input;
- const result = await search(source, keyword);
- return result.Manga.map((m) => ({
- status: m.Metadata.Status,
- title: m.Name,
- cover: m.Metadata.Cover,
- })).filter((m) => !!m.title);
+ const { result } = await search(source, keyword);
+ return result
+ .map((m) => ({
+ status: m.mangal.metadata.status,
+ title: m.mangal.name,
+ cover:
+ m.mangal.metadata.cover?.extraLarge || m.mangal.metadata.cover?.large || m.mangal.metadata.cover?.medium,
+ }))
+ .filter((m) => !!m.title);
}),
remove: t.procedure
.input(
@@ -128,7 +125,7 @@ export const mangaRouter = t.router({
)
.mutation(async ({ input, ctx }) => {
const { source, title, interval } = input;
- const mangaDetail: Manga | undefined = await getMangaDetail(source, title);
+ const mangaDetail = await getMangaDetail(source, title);
const library = await ctx.prisma.library.findFirst();
if (!mangaDetail || !library) {
throw new TRPCError({
@@ -148,7 +145,7 @@ export const mangaRouter = t.router({
});
}
- if (mangaDetail.Name !== title) {
+ if (mangaDetail.name !== title) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `${title} does not match the found manga.`,
@@ -162,7 +159,7 @@ export const mangaRouter = t.router({
},
data: {
source,
- title: mangaDetail.Name,
+ title: mangaDetail.name,
library: {
connect: {
id: library.id,
@@ -171,29 +168,32 @@ export const mangaRouter = t.router({
interval,
metadata: {
create: {
- cover: mangaDetail.Metadata.Cover,
- authors: mangaDetail.Metadata.Author ? [mangaDetail.Metadata.Author] : [],
- characters: mangaDetail.Metadata.Characters,
- genres: mangaDetail.Metadata.Genres,
- startDate: mangaDetail.Metadata.StartDate
+ cover:
+ mangaDetail.metadata.cover?.extraLarge ||
+ mangaDetail.metadata.cover?.large ||
+ mangaDetail.metadata.cover?.medium,
+ authors: mangaDetail.metadata.staff?.story ? [...mangaDetail.metadata.staff.story] : [],
+ characters: mangaDetail.metadata.characters,
+ genres: mangaDetail.metadata.genres,
+ startDate: mangaDetail.metadata.startDate
? new Date(
- mangaDetail.Metadata.StartDate.Year,
- mangaDetail.Metadata.StartDate.Month,
- mangaDetail.Metadata.StartDate.Day,
+ mangaDetail.metadata.startDate.year,
+ mangaDetail.metadata.startDate.month,
+ mangaDetail.metadata.startDate.day,
)
: undefined,
- endDate: mangaDetail.Metadata.EndDate
+ endDate: mangaDetail.metadata.endDate
? new Date(
- mangaDetail.Metadata.EndDate.Year,
- mangaDetail.Metadata.EndDate.Month,
- mangaDetail.Metadata.EndDate.Day,
+ mangaDetail.metadata.endDate.year,
+ mangaDetail.metadata.endDate.month,
+ mangaDetail.metadata.endDate.day,
)
: undefined,
- status: mangaDetail.Metadata.Status,
- summary: mangaDetail.Metadata.Summary,
- synonyms: mangaDetail.Metadata.Synonyms,
- tags: mangaDetail.Metadata.Tags,
- urls: mangaDetail.Metadata.URLs,
+ status: mangaDetail.metadata.status,
+ summary: mangaDetail.metadata.summary,
+ synonyms: mangaDetail.metadata.synonyms,
+ tags: mangaDetail.metadata.tags,
+ urls: mangaDetail.metadata.urls,
},
},
},
@@ -220,14 +220,18 @@ export const mangaRouter = t.router({
.mutation(async ({ input, ctx }) => {
const { id, interval, anilistId } = input;
const mangaInDb = await ctx.prisma.manga.findUniqueOrThrow({
+ include: {
+ library: true,
+ },
where: { id },
});
if (anilistId) {
await bindTitleToAnilistId(mangaInDb.title, anilistId);
+ await scheduleUpdateMetadata(mangaInDb.library.path, mangaInDb.title);
}
- const mangaDetail: Manga | undefined = await getMangaDetail(mangaInDb.source, mangaInDb.title);
+ const mangaDetail = await getMangaDetail(mangaInDb.source, mangaInDb.title);
if (!mangaDetail) {
throw new TRPCError({
code: 'NOT_FOUND',
@@ -240,29 +244,32 @@ export const mangaRouter = t.router({
id: mangaInDb.metadataId,
},
data: {
- cover: mangaDetail.Metadata.Cover,
- authors: mangaDetail.Metadata.Author ? [mangaDetail.Metadata.Author] : [],
- characters: mangaDetail.Metadata.Characters,
- genres: mangaDetail.Metadata.Genres,
- startDate: mangaDetail.Metadata.StartDate
+ cover:
+ mangaDetail.metadata.cover?.extraLarge ||
+ mangaDetail.metadata.cover?.large ||
+ mangaDetail.metadata.cover?.medium,
+ authors: mangaDetail.metadata.staff?.story ? [...mangaDetail.metadata.staff.story] : [],
+ characters: mangaDetail.metadata.characters,
+ genres: mangaDetail.metadata.genres,
+ startDate: mangaDetail.metadata.startDate
? new Date(
- mangaDetail.Metadata.StartDate.Year,
- mangaDetail.Metadata.StartDate.Month,
- mangaDetail.Metadata.StartDate.Day,
+ mangaDetail.metadata.startDate.year,
+ mangaDetail.metadata.startDate.month,
+ mangaDetail.metadata.startDate.day,
)
: undefined,
- endDate: mangaDetail.Metadata.EndDate
+ endDate: mangaDetail.metadata.endDate
? new Date(
- mangaDetail.Metadata.EndDate.Year,
- mangaDetail.Metadata.EndDate.Month,
- mangaDetail.Metadata.EndDate.Day,
+ mangaDetail.metadata.endDate.year,
+ mangaDetail.metadata.endDate.month,
+ mangaDetail.metadata.endDate.day,
)
: undefined,
- status: mangaDetail.Metadata.Status,
- summary: mangaDetail.Metadata.Summary,
- synonyms: mangaDetail.Metadata.Synonyms,
- tags: mangaDetail.Metadata.Tags,
- urls: mangaDetail.Metadata.URLs,
+ status: mangaDetail.metadata.status,
+ summary: mangaDetail.metadata.summary,
+ synonyms: mangaDetail.metadata.synonyms,
+ tags: mangaDetail.metadata.tags,
+ urls: mangaDetail.metadata.urls,
},
});
diff --git a/src/server/utils/mangal.ts b/src/server/utils/mangal.ts
index 8e5ba03..5605fa6 100644
--- a/src/server/utils/mangal.ts
+++ b/src/server/utils/mangal.ts
@@ -5,44 +5,65 @@ import { logger } from '../../utils/logging';
import { sanitizer } from '../../utils';
interface IOutput {
- Manga: Manga[];
+ result: Result[];
}
-export interface Manga {
- Name: string;
- URL: string;
- Index: number;
- ID: string;
- Chapters: Chapter[];
- Metadata: Metadata;
+interface Result {
+ source: string;
+ mangal: Mangal;
+}
+
+interface Mangal {
+ name: string;
+ url: string;
+ index: number;
+ id: string;
+ chapters: Chapter[];
+ metadata: Metadata;
}
interface Chapter {
- Name: string;
- URL: string;
- Index: number;
- ID: string;
- Volume: string;
+ name: string;
+ url: string;
+ index: number;
+ id: string;
+ volume: string;
}
interface Metadata {
- Genres: string[];
- Summary: string;
- Author: string;
- Cover: string;
- Tags: string[];
- Characters: string[];
- Status: string;
- StartDate: MangaDate;
- EndDate: MangaDate;
- Synonyms: string[];
- URLs: string[];
+ genres: string[];
+ summary: string;
+ staff: Staff;
+ cover: Cover;
+ bannerImage: string;
+ tags: string[];
+ characters: string[];
+ status: string;
+ startDate: MangaDate;
+ endDate: MangaDate;
+ synonyms: string[];
+ chapters: number;
+ urls: string[];
+}
+
+interface Cover {
+ extraLarge: string;
+ large: string;
+ medium: string;
+ color: string;
}
interface MangaDate {
- Year: number;
- Month: number;
- Day: number;
+ year: number;
+ month: number;
+ day: number;
+}
+
+interface Staff {
+ story: string[];
+ art: string[];
+ translation: string[];
+ lettering: string[];
}
interface ChapterFile {
@@ -51,9 +72,11 @@ interface ChapterFile {
fileName: string;
}
+export const getMangaPath = (libraryPath: string, title: string) => path.resolve(libraryPath, sanitizer(title));
+
export const getAvailableSources = async () => {
try {
- const { stdout, command } = await execa('mangal', ['sources', '-r']);
+ const { stdout, command } = await execa('mangal', ['sources', 'list', '-r']);
logger.info(`Get available sources with following command: ${command}`);
return stdout
.split('\n')
@@ -75,7 +98,20 @@ export const bindTitleToAnilistId = async (title: string, anilistId: string) =>
}
};
-export const getMangaPath = (libraryPath: string, title: string) => path.resolve(libraryPath, sanitizer(title));
+export const updateExistingMangaMetadata = async (libraryPath: string, title: string) => {
+ try {
+ const { command } = await execa('mangal', [
+ 'inline',
+ 'anilist',
+ 'update',
+ '--path',
+ getMangaPath(libraryPath, title),
+ ]);
+ logger.info(`Updated existing manga metadata: ${command}`);
+ } catch (err) {
+ logger.error(`Failed to update existing manga metadata. err: ${err}`);
+ }
+};
export const search = async (source: string, keyword: string): Promise => {
try {
@@ -87,7 +123,7 @@ export const search = async (source: string, keyword: string): Promise
}
return {
- Manga: [],
+ result: [],
};
};
@@ -106,9 +142,14 @@ export const getChaptersFromRemote = async (source: string, title: string): Prom
'-j',
]);
logger.info(`Get chapters with following command: ${command}`);
- const result: IOutput = JSON.parse(stdout);
- if (result && result.Manga.length === 1 && result.Manga[0]?.Chapters && result.Manga[0]?.Chapters.length > 0) {
- return result.Manga[0].Chapters.map((c) => c.Index - 1);
+ const output: IOutput = JSON.parse(stdout);
+ if (
+ output &&
+ output.result.length === 1 &&
+ output.result[0]?.mangal.chapters &&
+ output.result[0]?.mangal.chapters.length > 0
+ ) {
+ return output.result[0].mangal.chapters.map((c) => c.index - 1);
}
} catch (err) {
logger.error(err);
@@ -117,7 +158,7 @@ export const getChaptersFromRemote = async (source: string, title: string): Prom
return [];
};
-export const getMangaDetail = async (source: string, title: string): Promise => {
+export const getMangaDetail = async (source: string, title: string): Promise => {
try {
const { stdout, command } = await execa('mangal', [
'inline',
@@ -127,12 +168,14 @@ export const getMangaDetail = async (source: string, title: string): Promise