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

@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Added ### Added
- (**Migration**) Add a search option to ignore outdated matches - (**Migration**) Add a search option to ignore outdated matches
- (**Migration**) Add a search option to ignore matches with missing chapters
### Changed ### Changed

View File

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

View File

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

View File

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

View File

@@ -30,6 +30,7 @@ export const MigrationBulkSearchOptionsDialog = ({
const [selectHighestChapterNumberSource, setSelectHighestChapterNumberSource] = useState(false); const [selectHighestChapterNumberSource, setSelectHighestChapterNumberSource] = useState(false);
const [ignoreOutdatedMatches, setIgnoreOutdatedMatches] = useState(false); const [ignoreOutdatedMatches, setIgnoreOutdatedMatches] = useState(false);
const [ignoreWithMissingChapters, setIgnoreWithMissingChapters] = useState(false);
const [requireAdditionalChapters, setRequireAdditionalChapters] = useState(false); const [requireAdditionalChapters, setRequireAdditionalChapters] = useState(false);
const [performAdvancedSearch, setPerformAdvancedSearch] = useState(false); const [performAdvancedSearch, setPerformAdvancedSearch] = useState(false);
@@ -49,7 +50,7 @@ export const MigrationBulkSearchOptionsDialog = ({
<Typography <Typography
variant="body2" variant="body2"
color="textSecondary" 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> </Stack>
} }
sx={{ sx={{
@@ -70,7 +71,7 @@ export const MigrationBulkSearchOptionsDialog = ({
<Typography <Typography
variant="body2" variant="body2"
color="textSecondary" 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> </Stack>
} }
sx={{ sx={{
@@ -79,6 +80,27 @@ export const MigrationBulkSearchOptionsDialog = ({
checked={requireAdditionalChapters} checked={requireAdditionalChapters}
onChange={(_, checked) => setRequireAdditionalChapters(checked)} 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>
<DialogContent dividers> <DialogContent dividers>
<Stack <Stack
@@ -152,6 +174,7 @@ export const MigrationBulkSearchOptionsDialog = ({
selectHighestChapterNumberSource, selectHighestChapterNumberSource,
ignoreOutdatedMatches, ignoreOutdatedMatches,
requireAdditionalChapters, requireAdditionalChapters,
ignoreWithMissingChapters,
performAdvancedSearch, performAdvancedSearch,
}) })
} }

View File

@@ -11,22 +11,44 @@ import Typography from '@mui/material/Typography';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
export const MigrationEntryMetadataText = ( export const MigrationEntryMetadataText = (
entry: Pick<MigrationMatch, 'artist' | 'author' | 'latestChapterNumber'>, entry: Pick<MigrationMatch, 'artist' | 'author' | 'latestChapterNumber' | 'missingChapters'>,
) => { ) => {
const { t } = useLingui(); const { t } = useLingui();
const latestChapterNumber = (entry.latestChapterNumber ?? 0) > 1 ? entry.latestChapterNumber : t`Unknown`; 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 nodes = [
const author = entry.author ? `${entry.author} - ` : ''; entry.author && (
const isSameArtistAuthor = artist === author; <Typography sx={{ display: 'inline-flex' }} key="author" variant="inherit">
const artistAuthor = isSameArtistAuthor ? artist : `${artist}${author}`; {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 ( return (
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
{artistAuthor} {nodes}
{latestChapter}
</Typography> </Typography>
); );
}; };

View File

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

View File

@@ -518,6 +518,18 @@ msgstr "Automatically mark chapters as read when you download them."
msgid "Automatically refresh metadata" msgid "Automatically refresh metadata"
msgstr "Automatically refresh metadata" msgstr "Automatically refresh metadata"
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
msgid "Automatically select matches if they have additional chapters"
msgstr "Automatically select matches if they have additional chapters"
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
msgid "Automatically select matches if they have at least the same latest chapter"
msgstr "Automatically select matches if they have at least the same latest chapter"
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
msgid "Automatically select matches if they have no missing chapters"
msgstr "Automatically select matches if they have no missing chapters"
#: src/features/reader/settings/behaviour/ReaderBehaviourSettings.tsx #: src/features/reader/settings/behaviour/ReaderBehaviourSettings.tsx
msgid "Automatically use webtoon mode for entries that are detected to likely use the long strip format" msgid "Automatically use webtoon mode for entries that are detected to likely use the long strip format"
msgstr "Automatically use webtoon mode for entries that are detected to likely use the long strip format" msgstr "Automatically use webtoon mode for entries that are detected to likely use the long strip format"
@@ -1845,6 +1857,10 @@ msgstr "Ignore filters when searching"
msgid "Ignore matches that are behind in chapters" msgid "Ignore matches that are behind in chapters"
msgstr "Ignore matches that are behind in chapters" msgstr "Ignore matches that are behind in chapters"
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
msgid "Ignore matches with missing chapters"
msgstr "Ignore matches with missing chapters"
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx #: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
msgid "Ignore matches without newer chapters" msgid "Ignore matches without newer chapters"
msgstr "Ignore matches without newer chapters" msgstr "Ignore matches without newer chapters"
@@ -2375,6 +2391,11 @@ msgstr "Minute"
msgid "Misc" msgid "Misc"
msgstr "Misc" msgstr "Misc"
#. placeholder {0}: entry.missingChapters
#: src/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx
msgid "Missing: {0}"
msgstr "Missing: {0}"
#: src/features/reader/overlay/settings/ReaderSettingOverlayMode.tsx #: src/features/reader/overlay/settings/ReaderSettingOverlayMode.tsx
msgid "Mobile" msgid "Mobile"
msgstr "Mobile" msgstr "Mobile"
@@ -2558,14 +2579,6 @@ msgstr "Oneshot"
msgid "Ongoing" msgid "Ongoing"
msgstr "Ongoing" msgstr "Ongoing"
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
msgid "Only automatically select matches if they have additional chapters. They will still be shown in the found matches"
msgstr "Only automatically select matches if they have additional chapters. They will still be shown in the found matches"
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
msgid "Only automatically select matches if they have at least the same latest chapter. They will still be shown in the found matches"
msgstr "Only automatically select matches if they have at least the same latest chapter. They will still be shown in the found matches"
#: src/features/migration/Migration.constants.ts #: src/features/migration/Migration.constants.ts
msgid "Only outdated matches found" msgid "Only outdated matches found"
msgstr "Only outdated matches found" msgstr "Only outdated matches found"