Optionally ignore missing chapter matches during bulk migration

This commit is contained in:
schroda
2026-05-17 13:19:21 +02:00
parent fc4767e819
commit 710e7c0deb
8 changed files with 114 additions and 25 deletions

View File

@@ -109,7 +109,7 @@ export const MangaCard = memo((props: MangaCardProps) => {
if (isMigrateSelectMode) {
const isBulkMigrationManualSearch = !!onMigrateSelect;
if (isBulkMigrationManualSearch) {
onMigrateSelect(manga);
onMigrateSelect({ ...manga, missingChapters: undefined });
return;
}

View File

@@ -73,6 +73,7 @@ export interface MigrationBulkSearchSettings {
selectHighestChapterNumberSource: boolean;
ignoreOutdatedMatches: boolean;
requireAdditionalChapters: boolean;
ignoreWithMissingChapters: boolean;
performAdvancedSearch: boolean;
}
@@ -102,6 +103,7 @@ export interface MigrationMatch
extends MangaIdInfo, MangaTitleInfo, MangaThumbnailInfo, MangaSourceIdInfo, MangaArtistInfo, MangaAuthorInfo {
sourceTitle: SourceDisplayNameInfo['displayName'] | undefined;
latestChapterNumber: ChapterNumberInfo['chapterNumber'] | undefined;
missingChapters: number | undefined;
}
export interface TMigrationEntry {
@@ -110,6 +112,7 @@ export interface TMigrationEntry {
mangaArtist: MangaArtistInfo['artist'];
mangaAuthor: MangaAuthorInfo['author'];
latestChapterNumber: ChapterNumberInfo['chapterNumber'] | undefined;
missingChapters: number | undefined;
mangaThumbnailUrl: MangaThumbnailInfo['thumbnailUrl'] | undefined;
sourceId: SourceIdInfo['id'];
sourceTitle: SourceDisplayNameInfo['displayName'] | undefined;

View File

@@ -32,6 +32,7 @@ import {
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { GET_MIGRATION_SOURCE_MANGAS_FETCH } from '@/lib/graphql/source/SourceMutation.ts';
import type {
ChapterListFieldsFragment,
GetMigrationSourceMangasFetchMutation,
GetMigrationSourceMangasFetchMutationVariables,
GetServerSettingsQuery,
@@ -67,6 +68,7 @@ import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import isEqual from 'lodash/fp/isEqual';
import uniqBy from 'lodash/fp/uniqBy';
import { MigrationEntries } from '@/features/migration/MigrationEntries.ts';
import { Chapters } from '@/features/chapter/services/Chapters.ts';
const RESUMABLE_PHASES: readonly MigrationPhase[] = [MigrationPhase.SEARCHING, MigrationPhase.MIGRATING];
@@ -246,6 +248,7 @@ export class MigrationManager {
author: cachedEntry?.author ?? searchMatch.author,
sourceTitle: cachedEntry?.source?.displayName ?? searchMatch.sourceTitle,
latestChapterNumber: cachedEntry?.highestNumberedChapter?.chapterNumber ?? searchMatch.latestChapterNumber,
missingChapters: searchMatch.missingChapters,
};
}
@@ -262,6 +265,7 @@ export class MigrationManager {
mangaArtist: cachedEntry?.artist ?? entry.mangaArtist,
mangaAuthor: cachedEntry?.author ?? entry.mangaAuthor,
latestChapterNumber: cachedEntry?.highestNumberedChapter?.chapterNumber ?? entry.latestChapterNumber,
missingChapters: entry.missingChapters,
mangaThumbnailUrl: cachedEntry?.thumbnailUrl ?? entry.mangaThumbnailUrl,
sourceId: cachedEntry?.sourceId ?? entry.sourceId,
sourceTitle: cachedEntry?.source?.displayName ?? entry.sourceTitle,
@@ -336,6 +340,7 @@ export class MigrationManager {
mangaArtist: manga.artist,
mangaAuthor: manga.author,
latestChapterNumber: manga.highestNumberedChapter?.chapterNumber,
missingChapters: undefined,
mangaThumbnailUrl: manga.thumbnailUrl,
sourceId: manga.sourceId,
sourceTitle: manga.source?.displayName,
@@ -710,7 +715,7 @@ export class MigrationManager {
sourceId: SourceIdInfo['id'],
signal: AbortSignal,
{ selectHighestChapterNumberSource, performAdvancedSearch }: MigrationBulkSearchSettings,
): Promise<MangaMigrationFieldsFragment[]> {
): Promise<{ manga: MangaMigrationFieldsFragment; chapters: ChapterListFieldsFragment[] | null }[]> {
if (signal.aborted) {
throw new Error(signal.reason);
}
@@ -781,12 +786,20 @@ export class MigrationManager {
}).response,
);
return updatedMatch.data?.fetchManga?.manga ?? match;
if (updatedMatch.data?.fetchManga?.manga) {
return {
manga: updatedMatch.data.fetchManga.manga,
chapters: updatedMatch.data.fetchChapters?.chapters ?? null,
};
}
} catch (e) {
// ignore
}
return match;
return {
manga: match,
chapters: null,
};
})();
});
@@ -801,7 +814,12 @@ export class MigrationManager {
mainSignal: AbortSignal,
options: MigrationBulkSearchSettings,
): Promise<void> {
const { selectHighestChapterNumberSource, ignoreOutdatedMatches, requireAdditionalChapters } = options;
const {
selectHighestChapterNumberSource,
ignoreOutdatedMatches,
requireAdditionalChapters,
ignoreWithMissingChapters,
} = options;
const state = MigrationManager.getState();
const entry = state.entries[mangaId];
@@ -873,9 +891,11 @@ export class MigrationManager {
: null;
const newMatches = foundMatches.filter((newMatch) =>
draftEntry.searchMatches.every((existingMatch) => newMatch.id !== existingMatch.id),
draftEntry.searchMatches.every(
(existingMatch) => newMatch.manga.id !== existingMatch.id,
),
);
const matches = newMatches.map((manga) => ({
const matches = newMatches.map(({ manga, chapters }) => ({
id: manga.id,
title: manga.title,
artist: manga.artist,
@@ -884,6 +904,7 @@ export class MigrationManager {
thumbnailUrl: manga.thumbnailUrl,
sourceId: manga.sourceId,
sourceTitle: manga.source?.displayName,
missingChapters: chapters ? Chapters.getMissingCount(chapters) : undefined,
}));
draftEntry.destSourceIdToSearchState[destSourceId] = true;
@@ -921,6 +942,8 @@ export class MigrationManager {
const ignoreOutdatedMatch = ignoreOutdatedMatches && isOutdated;
const satisfiesRequireAdditionalChapters = !requireAdditionalChapters || hasNewerChapter;
const ignoreBecauseMissingChapters =
ignoreWithMissingChapters && !!bestMatch.missingChapters;
const isPreferredSourcePriorityMatch =
hasHigherSourcePriority && !selectHighestChapterNumberSource;
const isPreferredChapterNumberMatch =
@@ -933,6 +956,7 @@ export class MigrationManager {
!draftEntry.isManualSelection &&
!ignoreOutdatedMatch &&
satisfiesRequireAdditionalChapters &&
!ignoreBecauseMissingChapters &&
(isPreferredSourcePriorityMatch || isPreferredChapterNumberMatch);
if (isPreferredMatch) {
draftEntry.selectedMatchMangaId = bestMatch.id;

View File

@@ -30,6 +30,7 @@ export const MigrationBulkSearchOptionsDialog = ({
const [selectHighestChapterNumberSource, setSelectHighestChapterNumberSource] = useState(false);
const [ignoreOutdatedMatches, setIgnoreOutdatedMatches] = useState(false);
const [ignoreWithMissingChapters, setIgnoreWithMissingChapters] = useState(false);
const [requireAdditionalChapters, setRequireAdditionalChapters] = useState(false);
const [performAdvancedSearch, setPerformAdvancedSearch] = useState(false);
@@ -49,7 +50,7 @@ export const MigrationBulkSearchOptionsDialog = ({
<Typography
variant="body2"
color="textSecondary"
>{t`Only automatically select matches if they have at least the same latest chapter. They will still be shown in the found matches`}</Typography>
>{t`Automatically select matches if they have at least the same latest chapter`}</Typography>
</Stack>
}
sx={{
@@ -70,7 +71,7 @@ export const MigrationBulkSearchOptionsDialog = ({
<Typography
variant="body2"
color="textSecondary"
>{t`Only automatically select matches if they have additional chapters. They will still be shown in the found matches`}</Typography>
>{t`Automatically select matches if they have additional chapters`}</Typography>
</Stack>
}
sx={{
@@ -79,6 +80,27 @@ export const MigrationBulkSearchOptionsDialog = ({
checked={requireAdditionalChapters}
onChange={(_, checked) => setRequireAdditionalChapters(checked)}
/>
<CheckboxInput
label={
<Stack
sx={{
// Padding comes from the MUI Checkbox component
pt: '9px',
}}
>
<Typography>{t`Ignore matches with missing chapters`}</Typography>
<Typography
variant="body2"
color="textSecondary"
>{t`Automatically select matches if they have no missing chapters`}</Typography>
</Stack>
}
sx={{
alignItems: 'start',
}}
checked={ignoreWithMissingChapters}
onChange={(_, checked) => setIgnoreWithMissingChapters(checked)}
/>
</DialogContent>
<DialogContent dividers>
<Stack
@@ -152,6 +174,7 @@ export const MigrationBulkSearchOptionsDialog = ({
selectHighestChapterNumberSource,
ignoreOutdatedMatches,
requireAdditionalChapters,
ignoreWithMissingChapters,
performAdvancedSearch,
})
}

View File

@@ -11,22 +11,44 @@ import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react/macro';
export const MigrationEntryMetadataText = (
entry: Pick<MigrationMatch, 'artist' | 'author' | 'latestChapterNumber'>,
entry: Pick<MigrationMatch, 'artist' | 'author' | 'latestChapterNumber' | 'missingChapters'>,
) => {
const { t } = useLingui();
const latestChapterNumber = (entry.latestChapterNumber ?? 0) > 1 ? entry.latestChapterNumber : t`Unknown`;
const latestChapter = t`Latest: ${latestChapterNumber}`;
const isSameArtistAuthor = entry.artist === entry.author;
const artist = entry.artist ? `${entry.artist} - ` : '';
const author = entry.author ? `${entry.author} - ` : '';
const isSameArtistAuthor = artist === author;
const artistAuthor = isSameArtistAuthor ? artist : `${artist}${author}`;
const nodes = [
entry.author && (
<Typography sx={{ display: 'inline-flex' }} key="author" variant="inherit">
{entry.author}
</Typography>
),
!isSameArtistAuthor && entry.artist ? (
<Typography sx={{ display: 'inline-flex' }} key="artist" variant="inherit">
{entry.artist}
</Typography>
) : null,
<Typography sx={{ display: 'inline-flex' }} key="latest-chapter-number" variant="inherit">
{t`Latest: ${latestChapterNumber}`}
</Typography>,
entry.missingChapters ? (
<Typography sx={{ display: 'inline-flex' }} key="missing-chapters" variant="inherit" color="warning">
{t`Missing: ${entry.missingChapters}`}
</Typography>
) : null,
]
.filter((node) => node !== null)
.map((node, index, array) => (
<>
{node}
{index < array.length - 1 && ' - '}
</>
));
return (
<Typography variant="body2" color="textSecondary">
{artistAuthor}
{latestChapter}
{nodes}
</Typography>
);
};

View File

@@ -31,6 +31,7 @@ export const MigrationSourceEntry = memo((entry: TMigrationEntry) => {
mangaArtist,
mangaAuthor,
latestChapterNumber,
missingChapters,
searchMatches,
status,
} = entry;
@@ -50,6 +51,7 @@ export const MigrationSourceEntry = memo((entry: TMigrationEntry) => {
artist={mangaArtist}
author={mangaAuthor}
latestChapterNumber={latestChapterNumber}
missingChapters={missingChapters}
/>
</Stack>
<Box sx={{ display: 'flex', gap: 4, alignItems: 'center' }}>
@@ -91,6 +93,7 @@ export const MigrationSourceEntry = memo((entry: TMigrationEntry) => {
artist={mangaArtist}
author={mangaAuthor}
latestChapterNumber={latestChapterNumber}
missingChapters={missingChapters}
/>
</Stack>
</Box>