From ce7e93b0cd445df0935fdba61356c67a98634c7e Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Tue, 3 Sep 2024 01:16:52 +0200 Subject: [PATCH 01/10] misc: log error to console from error boundary --- src/components/ErrorBoundary.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 89e6e110a..77c79862a 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -41,6 +41,9 @@ const ErrorBoundary = ({ error, resetError }: { error?: Error, resetError?: () = const handleStableWebUiUpdate = useEventCallback(() => handleWebUiUpdate('Stable')); const handleDevWebUiUpdate = useEventCallback(() => handleWebUiUpdate('Dev')); + // eslint-disable-next-line no-console + console.log(error, routeError); + return (
@@ -50,7 +53,7 @@ const ErrorBoundary = ({ error, resetError }: { error?: Error, resetError?: () = The information below is absolutely (maybe) useless!

- {error?.stack ?? routeError.data} + {error ? `${error.message}\n${error.stack}` : routeError.data} {routeError?.status === 404 ? ( From 9f48e10230a3a95c3dd34f872f2eb84fcac02df1 Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Tue, 3 Sep 2024 01:36:49 +0200 Subject: [PATCH 02/10] fix: use online endpoints to fetch movie data --- src/core/react-query/tmdb/queries.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/react-query/tmdb/queries.ts b/src/core/react-query/tmdb/queries.ts index acfdbe99b..5830ce69a 100644 --- a/src/core/react-query/tmdb/queries.ts +++ b/src/core/react-query/tmdb/queries.ts @@ -64,8 +64,8 @@ export const useTmdbShowEpisodesQuery = (showId: number, params: TmdbShowEpisode export const useTmdbShowOrMovieQuery = (tmdbId: number, type: 'Show' | 'Movie', enabled = true) => useQuery({ - queryKey: ['series', 'tmdb', 'show', tmdbId, type], - queryFn: () => axios.get(`Tmdb/${type}/${tmdbId}`), + queryKey: ['series', 'tmdb', enabled ? type.toLowerCase() : 'unknown', tmdbId], + queryFn: () => axios.get(type === 'Movie' ? `Tmdb/Movie/Online/${tmdbId}` : `Tmdb/Show/${tmdbId}`), enabled, }); @@ -135,9 +135,9 @@ export const useTmdbBulkEpisodesQuery = (data: TmdbBulkRequestType, enabled = tr }; }; -export const useTmdbBulkMoviesQuery = (data: TmdbBulkRequestType, enabled = true) => +export const useTmdbBulkMoviesOnlineQuery = (data: TmdbBulkRequestType, enabled = true) => useQuery({ queryKey: ['series', 'tmdb', 'movie', 'bulk', data], - queryFn: () => axios.post('Tmdb/Movie/Bulk', data), + queryFn: () => axios.post('Tmdb/Movie/Online/Bulk', data), enabled: enabled && data.IDs.length > 0, }); From 00848bbbe7336915359e00bf1efaffc2b137e42a Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Tue, 3 Sep 2024 01:37:03 +0200 Subject: [PATCH 03/10] misc: move tmdb add link button right below existing links --- .../collection/series/SeriesOverview.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pages/collection/series/SeriesOverview.tsx b/src/pages/collection/series/SeriesOverview.tsx index 5325a8202..98578d75c 100644 --- a/src/pages/collection/series/SeriesOverview.tsx +++ b/src/pages/collection/series/SeriesOverview.tsx @@ -92,16 +92,20 @@ const SeriesOverview = () => { return ; } - return flatMap(tmdbIds, (ids, type: 'Movie' | 'Show') => - ids.map(id => ( - - ))); + return [ + ...flatMap(tmdbIds, (ids, type: 'Movie' | 'Show') => + ids.map(id => ( + + ))), + /* Show row to add new TMDB links */ + , + ]; } // Site is not TMDB, so it's either a single ID or an array of IDs @@ -112,10 +116,6 @@ const SeriesOverview = () => { )); })} - - {/* Show row to add TMDB link if a link already exists */} - {(series.IDs.TMDB.Show.length !== 0 - || series.IDs.TMDB.Movie.length !== 0) && }
) : ( From 21f9f35308dab2fb7e70758f89e0ac0f804c56bd Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Tue, 3 Sep 2024 01:39:14 +0200 Subject: [PATCH 04/10] misc: split D/T into D and T --- src/components/Collection/Tmdb/MatchRating.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Collection/Tmdb/MatchRating.tsx b/src/components/Collection/Tmdb/MatchRating.tsx index bbbf262fc..d8776253d 100644 --- a/src/components/Collection/Tmdb/MatchRating.tsx +++ b/src/components/Collection/Tmdb/MatchRating.tsx @@ -8,8 +8,9 @@ const getAbbreviation = (rating?: MatchRatingType) => { case MatchRatingType.DateAndTitleMatches: return 'DT'; case MatchRatingType.DateMatches: + return 'D'; case MatchRatingType.TitleMatches: - return 'D/T'; + return 'T'; case MatchRatingType.UserVerified: return 'UO'; case MatchRatingType.FirstAvailable: From 7d946c40e92773f5eae9961357e6181394e7b672 Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Tue, 3 Sep 2024 02:17:22 +0200 Subject: [PATCH 05/10] feat: add 1-n mapping --- .../Collection/Tmdb/AniDBEpisode.tsx | 37 +++++- src/components/Collection/Tmdb/EpisodeRow.tsx | 66 ++++++---- .../Collection/Tmdb/EpisodeSelect.tsx | 2 +- .../Collection/Tmdb/MatchRating.tsx | 6 +- src/components/Collection/Tmdb/MovieRow.tsx | 21 +-- src/components/Collection/Tmdb/TopPanel.tsx | 17 ++- src/core/types/api/tmdb.ts | 2 +- src/pages/collection/series/TmdbLinking.tsx | 124 ++++++++++++------ 8 files changed, 188 insertions(+), 87 deletions(-) diff --git a/src/components/Collection/Tmdb/AniDBEpisode.tsx b/src/components/Collection/Tmdb/AniDBEpisode.tsx index e989fd349..268012af5 100644 --- a/src/components/Collection/Tmdb/AniDBEpisode.tsx +++ b/src/components/Collection/Tmdb/AniDBEpisode.tsx @@ -1,6 +1,9 @@ import React from 'react'; +import { mdiMinus, mdiPlus } from '@mdi/js'; +import { Icon } from '@mdi/react'; import cx from 'classnames'; +import Button from '@/components/Input/Button'; import { padNumber } from '@/core/util'; import { getEpisodePrefixAlt } from '@/core/utilities/getEpisodePrefix'; @@ -8,21 +11,43 @@ import type { EpisodeType } from '@/core/types/api/episode'; type Props = { episode: EpisodeType; + extra?: boolean; isOdd: boolean; + onIconClick?: () => void; }; -const AniDBEpisode = React.memo(({ episode, isOdd }: Props) => ( +const AniDBEpisode = React.memo(({ episode, extra, isOdd, onIconClick }: Props) => (
-
- {getEpisodePrefixAlt(episode.AniDB?.Type)} -
-
{padNumber(episode.AniDB?.EpisodeNumber ?? 0)}
-
{episode.Name}
+ {!extra && ( + <> +
+ {getEpisodePrefixAlt(episode.AniDB?.Type)} +
+
{padNumber(episode.AniDB?.EpisodeNumber ?? 0)}
+
{episode.Name}
+ {onIconClick && ( + + )} + + )} + + {extra && ( + <> +
+ {onIconClick && ( + + )} + + )}
)); diff --git a/src/components/Collection/Tmdb/EpisodeRow.tsx b/src/components/Collection/Tmdb/EpisodeRow.tsx index 1cd8acf65..8b6ea4a78 100644 --- a/src/components/Collection/Tmdb/EpisodeRow.tsx +++ b/src/components/Collection/Tmdb/EpisodeRow.tsx @@ -18,74 +18,96 @@ import type { Updater } from 'use-immer'; type Props = { episode: EpisodeType; isOdd: boolean; - overrides: Record; - setLinkOverrides: Updater>; + offset: number; + overrides: Record; + setLinkOverrides: Updater>; tmdbEpisodesPending: boolean; - xrefs?: TmdbEpisodeXrefType[]; + xref?: TmdbEpisodeXrefType; + xrefsPending: boolean; }; const EpisodeRow = React.memo((props: Props) => { const { episode, isOdd, + offset, overrides, setLinkOverrides, tmdbEpisodesPending, - xrefs, + xref, + xrefsPending, } = props; - // TODO: Add support for 1-n (1 AniDB - n TMDB) mapping - const xref = useMemo( - () => xrefs?.find(ref => ref.AnidbEpisodeID === episode.IDs.AniDB), - [episode.IDs.AniDB, xrefs], - ); - // This does not actually query the server. We already queried it in the parent component // This just gets the data from the cache const tmdbEpisodesQuery = useTmdbBulkEpisodesQuery({ IDs: [] }); const tmdbEpisode = useMemo(() => { - if (!xref || !('TmdbEpisodeID' in xref)) return undefined; + if (!xref || xref.TmdbEpisodeID === 0) return undefined; return find(tmdbEpisodesQuery.data, { ID: xref.TmdbEpisodeID }); }, [tmdbEpisodesQuery.data, xref]); const isPending = useMemo( () => { // Xrefs are not loaded yet - if (!xrefs) return true; + if (xrefsPending) return true; // Xrefs are loaded but episode doesn't have an xref if (!xref) return false; return !tmdbEpisode && tmdbEpisodesPending; }, - [tmdbEpisode, tmdbEpisodesPending, xref, xrefs], + [tmdbEpisode, tmdbEpisodesPending, xref, xrefsPending], ); + const addLink = useEventCallback(() => { + const anidbEpisodeId = episode.IDs.AniDB; + setLinkOverrides((draftState) => { + if (!draftState[anidbEpisodeId]) draftState[anidbEpisodeId] = [undefined]; + draftState[anidbEpisodeId]![draftState[anidbEpisodeId]!.length] = undefined; + }); + }); + + const removeLink = useEventCallback(() => { + setLinkOverrides((draftState) => { + if (!draftState[episode.IDs.AniDB]) draftState[episode.IDs.AniDB] = [undefined]; + draftState[episode.IDs.AniDB]!.splice(offset, 1); + }); + }); + const overrideLink = useEventCallback((newTmdbId?: number) => { const anidbEpisodeId = episode.IDs.AniDB; setLinkOverrides((draftState) => { - if (newTmdbId === null || newTmdbId === undefined || newTmdbId === tmdbEpisode?.ID) { - delete draftState[anidbEpisodeId]; - } else draftState[anidbEpisodeId] = newTmdbId; + if (!draftState[anidbEpisodeId]) draftState[anidbEpisodeId] = [undefined]; + if ( + newTmdbId === null || newTmdbId === undefined + || (newTmdbId === tmdbEpisode?.ID && draftState[anidbEpisodeId]![offset] !== undefined) + ) { + draftState[anidbEpisodeId]![offset] = undefined; + } else draftState[anidbEpisodeId]![offset] = newTmdbId; }); }); const matchRating = useMemo(() => { if (isPending) return undefined; - if (overrides[episode.IDs.AniDB] === 0) return undefined; - if (overrides[episode.IDs.AniDB]) return MatchRatingType.UserVerified; + if (overrides[episode.IDs.AniDB]?.[offset] === 0) return undefined; + if (overrides[episode.IDs.AniDB]?.[offset]) return MatchRatingType.UserVerified; return xref?.Rating; - }, [episode, isPending, overrides, xref]); + }, [episode, isPending, overrides, xref, offset]); return ( <> - + 0} + onIconClick={offset === 0 ? addLink : removeLink} + /> - + {!isPending && ( diff --git a/src/components/Collection/Tmdb/EpisodeSelect.tsx b/src/components/Collection/Tmdb/EpisodeSelect.tsx index 733112fae..072a5ed55 100644 --- a/src/components/Collection/Tmdb/EpisodeSelect.tsx +++ b/src/components/Collection/Tmdb/EpisodeSelect.tsx @@ -19,7 +19,7 @@ import type { TmdbEpisodeType } from '@/core/types/api/tmdb'; type Props = { isOdd: boolean; overrideLink: (newTmdbId?: number) => void; - override: number; + override: number | undefined; tmdbEpisode?: TmdbEpisodeType; }; diff --git a/src/components/Collection/Tmdb/MatchRating.tsx b/src/components/Collection/Tmdb/MatchRating.tsx index d8776253d..1b9665ee2 100644 --- a/src/components/Collection/Tmdb/MatchRating.tsx +++ b/src/components/Collection/Tmdb/MatchRating.tsx @@ -20,15 +20,17 @@ const getAbbreviation = (rating?: MatchRatingType) => { } }; -const MatchRating = ({ rating }: { rating?: MatchRatingType }) => ( +const MatchRating = ({ isOdd, rating }: { isOdd: boolean, rating?: MatchRatingType }) => (
diff --git a/src/components/Collection/Tmdb/MovieRow.tsx b/src/components/Collection/Tmdb/MovieRow.tsx index 724bb1e52..d79dce1bc 100644 --- a/src/components/Collection/Tmdb/MovieRow.tsx +++ b/src/components/Collection/Tmdb/MovieRow.tsx @@ -7,7 +7,7 @@ import { find, map, toNumber } from 'lodash'; import AniDBEpisode from '@/components/Collection/Tmdb/AniDBEpisode'; import Button from '@/components/Input/Button'; -import { useTmdbBulkMoviesQuery, useTmdbShowOrMovieQuery } from '@/core/react-query/tmdb/queries'; +import { useTmdbBulkMoviesOnlineQuery, useTmdbShowOrMovieQuery } from '@/core/react-query/tmdb/queries'; import useEventCallback from '@/hooks/useEventCallback'; import type { EpisodeType } from '@/core/types/api/episode'; @@ -17,8 +17,8 @@ import type { Updater } from 'use-immer'; type Props = { episode: EpisodeType; isOdd: boolean; - overrides: Record; - setLinkOverrides: Updater>; + overrides: Record; + setLinkOverrides: Updater>; xrefs?: TmdbMovieXrefType[]; }; @@ -34,19 +34,19 @@ const MovieRow = React.memo((props: Props) => { ); const tmdbMovieQuery = useTmdbShowOrMovieQuery(tmdbId, 'Movie'); - const tmdbBulkMoviesQuery = useTmdbBulkMoviesQuery( + const tmdbBulkMoviesQuery = useTmdbBulkMoviesOnlineQuery( { IDs: map(xrefs, item => item.TmdbMovieID) }, ); const tmdbMovie = useMemo(() => { if (!tmdbMovieQuery.data && !tmdbBulkMoviesQuery.data) return undefined; const override = overrides[episode.IDs.AniDB]; - if (override === 0) return undefined; + if (override?.[0] === 0) return undefined; const tmdbMovies = [tmdbMovieQuery.data, ...(tmdbBulkMoviesQuery.data ?? [])]; - if (override) { - return find(tmdbMovies, { ID: override }); + if (override?.[0]) { + return find(tmdbMovies, { ID: override[0] }); } if (!xref) return undefined; @@ -68,11 +68,12 @@ const MovieRow = React.memo((props: Props) => { setLinkOverrides((draftState) => { const anidbEpisodeId = episode.IDs.AniDB; const newTmdbId = tmdbMovie?.ID ? 0 : tmdbId; + if (!draftState[anidbEpisodeId]) draftState[anidbEpisodeId] = [undefined]; // If already linked episode was unlinked and linked again, remove override - if (draftState[anidbEpisodeId] === 0 && newTmdbId === tmdbId) delete draftState[anidbEpisodeId]; + if (draftState[anidbEpisodeId]![0] === 0 && newTmdbId === tmdbId) delete draftState[anidbEpisodeId]; // If new link was created and removed, remove override - else if (draftState[anidbEpisodeId] === tmdbId && newTmdbId === 0) delete draftState[anidbEpisodeId]; - else draftState[anidbEpisodeId] = newTmdbId; + else if (draftState[anidbEpisodeId]![0] === tmdbId && newTmdbId === 0) delete draftState[anidbEpisodeId]; + else draftState[anidbEpisodeId]![0] = newTmdbId; }); }); diff --git a/src/components/Collection/Tmdb/TopPanel.tsx b/src/components/Collection/Tmdb/TopPanel.tsx index 93fed21fa..ad7fce769 100644 --- a/src/components/Collection/Tmdb/TopPanel.tsx +++ b/src/components/Collection/Tmdb/TopPanel.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { mdiLinkPlus } from '@mdi/js'; import { Icon } from '@mdi/react'; import cx from 'classnames'; -import { countBy } from 'lodash'; +import { countBy, flatMap } from 'lodash'; import Button from '@/components/Input/Button'; import ShokoPanel from '@/components/Panels/ShokoPanel'; @@ -17,7 +17,7 @@ type Props = { disableCreateLink: boolean; handleCreateLink: () => void; seriesId: number; - xrefs?: TmdbEpisodeXrefType[]; + xrefs?: Record; xrefsCount?: number; }; @@ -25,18 +25,23 @@ const TopPanel = (props: Props) => { const { createInProgress, disableCreateLink, handleCreateLink, seriesId, xrefs, xrefsCount } = props; const navigate = useNavigate(); + const flatXrefs = useMemo(() => (xrefs ? flatMap(xrefs, x => x) as TmdbEpisodeXrefType[] : undefined), [xrefs]); + const matchRatingCounts = useMemo( - () => (xrefs ? countBy(xrefs, 'Rating') : {}), - [xrefs], + () => (flatXrefs ? countBy(flatXrefs, 'Rating') : {}), + [flatXrefs], ) as Record; return ( - }> + } + >
diff --git a/src/core/types/api/tmdb.ts b/src/core/types/api/tmdb.ts index 9f98a439a..e8602e80c 100644 --- a/src/core/types/api/tmdb.ts +++ b/src/core/types/api/tmdb.ts @@ -28,7 +28,7 @@ export type TmdbXrefType = { export type TmdbEpisodeXrefType = { TmdbShowID: number; - TmdbEpisodeID?: number; + TmdbEpisodeID: number; Index: number; Rating: MatchRatingType; } & TmdbXrefType; diff --git a/src/pages/collection/series/TmdbLinking.tsx b/src/pages/collection/series/TmdbLinking.tsx index a9ae7183b..6724759bb 100644 --- a/src/pages/collection/series/TmdbLinking.tsx +++ b/src/pages/collection/series/TmdbLinking.tsx @@ -1,11 +1,11 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router'; import { useNavigate, useOutletContext, useSearchParams } from 'react-router-dom'; import { mdiLoading, mdiOpenInNew, mdiPencilCircleOutline } from '@mdi/js'; import { Icon } from '@mdi/react'; import { useVirtualizer } from '@tanstack/react-virtual'; import cx from 'classnames'; -import { debounce, map, reduce, toNumber } from 'lodash'; +import { debounce, filter, flatMap, groupBy, map, reduce, some, toNumber } from 'lodash'; import { useImmer } from 'use-immer'; import AniDBEpisode from '@/components/Collection/Tmdb/AniDBEpisode'; @@ -34,6 +34,7 @@ import useEventCallback from '@/hooks/useEventCallback'; import useFlattenListResult from '@/hooks/useFlattenListResult'; import type { SeriesContextType } from '@/components/Collection/constants'; +import type { TmdbEpisodeXrefType } from '@/core/types/api/tmdb'; const TmdbLinking = () => { const seriesId = toNumber(useParams().seriesId); @@ -74,7 +75,12 @@ const TmdbLinking = () => { { tmdbShowID: tmdbId, pageSize: 0 }, !createInProgress && !!seriesId && type === 'Show' && !!seriesQuery.data, ); - const episodeXrefs = useMemo(() => episodeXrefsQuery.data?.List, [episodeXrefsQuery.data]); + const episodeXrefs = useMemo( + () => (episodeXrefsQuery.data + ? groupBy(episodeXrefsQuery.data.List, 'AnidbEpisodeID') as Record + : undefined), + [episodeXrefsQuery.data], + ); const movieXrefsQuery = useTmdbMovieXrefsQuery( seriesId, @@ -90,10 +96,9 @@ const TmdbLinking = () => { const lastPageAnidbIds = lastPage.List.map(episode => episode.IDs.AniDB); - return episodeXrefs - .filter(xref => lastPageAnidbIds.includes(xref.AnidbEpisodeID)) - .map(xref => xref.TmdbEpisodeID) - .filter(tmdbEpisodeId => !!tmdbEpisodeId) as number[]; + return filter(episodeXrefs, xrefs => some(xrefs, xref => lastPageAnidbIds.includes(xref.AnidbEpisodeID))) + .flatMap(xrefs => map(xrefs, xref => xref.TmdbEpisodeID)) + .filter(tmdbEpisodeId => !!tmdbEpisodeId); }, [episodeXrefs, episodesQuery.data, type], ); @@ -107,10 +112,22 @@ const TmdbLinking = () => { const { scrollRef } = useOutletContext(); + const [ + linkOverrides, + setLinkOverrides, + ] = useImmer({} as Record); + + const estimateSize = useEventCallback((i: number) => { + const episode = episodes[i]; + if (!episode) return 56; // 56px is the minimum height of a loaded row. + const overrides = linkOverrides[episode.IDs.AniDB] ?? [undefined]; + return 56 * overrides.length; + }); + const rowVirtualizer = useVirtualizer({ count: episodeCount, getScrollElement: () => scrollRef.current, - estimateSize: () => 56, // 56px is the minimum height of a loaded row, + estimateSize, overscan: 10, gap: 8, }); @@ -124,26 +141,23 @@ const TmdbLinking = () => { [fetchNextEpisodesPage], ); - const [ - linkOverrides, - setLinkOverrides, - ] = useImmer>({}); - - const finalXrefs = useMemo>( + const finalXrefs = useMemo>( () => { const tempXrefs: Record = {}; const movieXrefs = movieXrefsQuery.data ?? []; movieXrefs .filter((xref) => { - if (linkOverrides[xref.AnidbEpisodeID] !== undefined) return linkOverrides[xref.AnidbEpisodeID] === tmdbId; + if (linkOverrides[xref.AnidbEpisodeID]?.[0] !== undefined) { + return linkOverrides[xref.AnidbEpisodeID]![0] === tmdbId; + } return xref.TmdbMovieID === tmdbId; }) .forEach((xref) => { tempXrefs[xref.AnidbEpisodeID] = xref.TmdbMovieID; }); - const finalLinkOverrides = { ...linkOverrides }; + const finalLinkOverrides = Object.fromEntries(map(linkOverrides, (i, a) => [a, i?.[0]])); Object.keys(finalLinkOverrides).forEach((episodeId) => { if (linkOverrides[episodeId] === 0) delete finalLinkOverrides[episodeId]; }); @@ -158,7 +172,7 @@ const TmdbLinking = () => { const { mutateAsync: deleteLink } = useDeleteTmdbLinkMutation(seriesId, type ?? 'Show'); const { mutateAsync: createAutoLinks } = useTmdbAddAutoXrefsMutation(seriesId); - const createLinks = useEventCallback(async () => { + const createEpisodeLinks = useEventCallback(async () => { setCreateInProgress(true); try { if (isNewLink) { @@ -168,21 +182,22 @@ const TmdbLinking = () => { if (Object.keys(linkOverrides).length > 0) { // The two commented lines below can be used if we need 1-n mappings - // const set = new Set(); + const set = new Set(); await editEpisodeLinks({ ResetAll: false, - Mapping: map(linkOverrides, (overrideId, episodeId) => ({ - AniDBID: toNumber(episodeId), - TmdbID: overrideId, - Replace: true, - // Replace: !set.has(episodeId) ? Booelan(set.add(episodeId)) : false, - })), + Mapping: flatMap(linkOverrides, (overrideIds, episodeId) => (overrideIds + ? map(overrideIds.filter((a): a is number => a !== undefined), overrideId => ({ + AniDBID: toNumber(episodeId), + TmdbID: overrideId, + Replace: !set.has(episodeId) ? Boolean(set.add(episodeId)) : false, + })) + : [])), }); } resetQueries(['series', seriesId]); setLinkOverrides({}); - toast.success('Links saved!'); + toast.success('Series has been linked and TMDB related tasks for data and images have been added to the queue!'); } catch (error) { toast.error('Failed to save links!'); } @@ -194,8 +209,8 @@ const TmdbLinking = () => { try { const linkGroups = reduce( linkOverrides, - (result, overrideId, episodeId) => { - if (overrideId) result.create.push(toNumber(episodeId)); + (result, overrideIds, episodeId) => { + if (overrideIds && some(overrideIds, a => a !== undefined)) result.create.push(toNumber(episodeId)); else result.delete.push(toNumber(episodeId)); return result; }, @@ -228,7 +243,7 @@ const TmdbLinking = () => { return; } - createLinks().catch(console.error); + createEpisodeLinks().catch(console.error); }); const disableCreateLink = useMemo(() => { @@ -243,6 +258,19 @@ const TmdbLinking = () => { setLinkOverrides({}); }); + useEffect(() => { + setLinkOverrides( + episodeXrefs + ? Object.fromEntries( + map( + episodeXrefs, + (xrefs, episodeId) => [episodeId, new Array(xrefs?.length ?? 1)], + ), + ) + : {}, + ); + }, [episodeXrefs, setLinkOverrides]); + return (
{ handleCreateLink={handleCreateLink} seriesId={seriesId} xrefs={type === 'Show' ? episodeXrefs : undefined} - xrefsCount={type === 'Show' ? episodeXrefs?.length : Object.keys(finalXrefs).length} + xrefsCount={type === 'Show' ? undefined : Object.keys(finalXrefs).length} />
{(seriesQuery.isPending || episodesQuery.isPending) && ( @@ -299,7 +327,7 @@ const TmdbLinking = () => { > {tmdbId}  -  - {tmdbShowOrMovieQuery.data?.Title} + {tmdbShowOrMovieQuery.data.Title}
@@ -335,9 +363,13 @@ const TmdbLinking = () => { if (!episode && !episodesQuery.isFetchingNextPage) fetchNextPageDebounced(); + const overrides = linkOverrides[episode.IDs.AniDB] ?? [undefined]; return (
{ data-index={virtualItem.index} > {episode && type === 'Show' && ( - + map( + overrides, + (_, i) => ( +
+ +
+ ), + ) )} {episode && type === 'Movie' && ( From 3e58d61e51ba710355818d339695eb190092428b Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Tue, 3 Sep 2024 02:29:53 +0200 Subject: [PATCH 06/10] misc: bump min server version --- src/core/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/util.ts b/src/core/util.ts index baf57bb64..963539b1b 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -29,7 +29,7 @@ export function isDebug() { return DEV; } -export const minimumSupportedServerVersion = '4.2.2.112'; +export const minimumSupportedServerVersion = '4.2.2.116'; export const parseServerVersion = (version: string) => { const semverVersion = semver.coerce(version)?.raw; From 64f3b4fa4bd6452401205f5d2e6023d2db1f9919 Mon Sep 17 00:00:00 2001 From: Harshith Mohan <26010946+harshithmohan@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:49:38 +0530 Subject: [PATCH 07/10] Add eslint rule to disallow <3 length variable names --- .eslintrc.json | 3 +- .../BackgroundImagePlaceholderDiv.tsx | 14 +- src/components/Collection/CollectionView.tsx | 10 +- .../Collection/Files/FilesMissingEpisodes.tsx | 2 +- .../Group/EditGroupTabs/NameTab.tsx | 4 +- .../Group/EditGroupTabs/SeriesTab.tsx | 5 +- .../Series/EditSeriesTabs/GroupTab.tsx | 2 +- .../Series/EditSeriesTabs/NameTab.tsx | 2 +- .../Series/EditSeriesTabs/PersonalStats.tsx | 130 ------------------ src/components/Collection/TitleOptions.tsx | 2 +- src/components/ErrorBoundary.tsx | 3 +- src/components/Panels/ModalPanel.tsx | 2 +- src/components/Settings/AvatarEditorModal.tsx | 2 +- src/components/Utilities/FilesSummary.tsx | 8 +- .../ReleaseManagement/MultiplesUtilList.tsx | 6 +- .../Utilities/Renamer/AddFilesModal.tsx | 4 +- .../Utilities/Renamer/ConfigModal.tsx | 4 +- .../SeriesWithoutFiles/AddSeriesModal.tsx | 4 +- .../Unrecognized/AvDumpSeriesSelectModal.tsx | 4 +- .../Unrecognized/ManuallyLinkedFilesRow.tsx | 6 +- .../Utilities/Unrecognized/RangeFillModal.tsx | 4 +- src/components/Utilities/UtilitiesTable.tsx | 6 +- src/core/util.ts | 2 +- src/core/utilities/auto-match-logic.ts | 10 +- src/hooks/useEventCallback.ts | 6 +- src/pages/collection/series/SeriesCredits.tsx | 14 +- src/pages/collection/series/SeriesTags.tsx | 2 +- .../dashboard/components/WelcomeModal.tsx | 2 +- src/pages/dashboard/panels/MediaType.tsx | 2 +- src/pages/firstrun/LocalAccount.tsx | 4 +- src/pages/login/LoginPage.tsx | 6 +- src/pages/logs/LogsPage.tsx | 2 +- src/pages/utilities/FileSearch.tsx | 2 +- src/pages/utilities/Renamer.tsx | 2 +- .../utilities/SeriesWithoutFilesUtility.tsx | 6 +- .../IgnoredFilesTab.tsx | 2 +- .../UnrecognizedUtilityTabs/LinkFilesTab.tsx | 50 ++++--- .../ManuallyLinkedTab.tsx | 4 +- .../UnrecognizedTab.tsx | 2 +- 39 files changed, 115 insertions(+), 230 deletions(-) delete mode 100644 src/components/Collection/Series/EditSeriesTabs/PersonalStats.tsx diff --git a/.eslintrc.json b/.eslintrc.json index 642b24462..2f602c99c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -60,6 +60,7 @@ "ignorePrimitives": { "boolean": true } } ], + "id-length": ["error", { "min": 3, "exceptions": ["cx", "ID", "id", "_", "__"], "properties": "never" }], "implicit-arrow-linebreak": "off", "import/order": [ "error", @@ -94,7 +95,7 @@ ], "no-param-reassign": [ "error", - { "props": true, "ignorePropertyModificationsFor": ["sliceState", "immerState", "draftState", "draftState2"] } + { "props": true, "ignorePropertyModificationsFor": ["sliceState", "immerState", "draftState", "draftState2", "reduceResult"] } ], "no-restricted-imports": [ "error", diff --git a/src/components/BackgroundImagePlaceholderDiv.tsx b/src/components/BackgroundImagePlaceholderDiv.tsx index 14d1dcd5a..67348f7ec 100644 --- a/src/components/BackgroundImagePlaceholderDiv.tsx +++ b/src/components/BackgroundImagePlaceholderDiv.tsx @@ -63,19 +63,19 @@ const BackgroundImagePlaceholderDiv = React.memo((props: Props) => { setImageError(null); let complete = false; - const bg = new Image(); - bg.setAttribute('lazy', 'true'); - bg.onload = () => { + const background = new Image(); + background.setAttribute('lazy', 'true'); + background.onload = () => { if (complete) return; complete = true; - setBackgroundImage(bg); + setBackgroundImage(background); }; - bg.onerror = () => { + background.onerror = () => { if (complete) return; complete = true; setImageError('Please refresh your browser to correct.'); }; - bg.src = imageSource; + background.src = imageSource; return () => { complete = true; }; @@ -116,7 +116,7 @@ const BackgroundImagePlaceholderDiv = React.memo((props: Props) => { aria-label="Link to image" rel="noopener noreferrer" target="_blank" - onClick={e => e.stopPropagation()} + onClick={event => event.stopPropagation()} > diff --git a/src/components/Collection/CollectionView.tsx b/src/components/Collection/CollectionView.tsx index 716c32ce3..e3c4abaca 100644 --- a/src/components/Collection/CollectionView.tsx +++ b/src/components/Collection/CollectionView.tsx @@ -114,17 +114,17 @@ const CollectionView = (props: Props) => { const toIndex = fromIndex + itemsPerRow; // Here, i will be the actual index of the group in group list - for (let i = fromIndex; i < toIndex; i += 1) { - const item = items[i]; + for (let index = fromIndex; index < toIndex; index += 1) { + const item = items[index]; // Placeholder to solve formatting issues. // Used to fill the empty "slots" in the last row - const isPlaceholder = i > total - 1; + const isPlaceholder = index > total - 1; if (isPlaceholder) { children.push(
{ children.push(
( {padNumber(episode.EpisodeNumber)}
- {episode.Titles.find(e => e.Language === 'en')?.Name ?? '--'} + {episode.Titles.find(title => title.Language === 'en')?.Name ?? '--'}   { toggleNameEditable(); }); - const updateName = useEventCallback((e: React.ChangeEvent) => { - setGroupName(e.target.value); + const updateName = useEventCallback((event: React.ChangeEvent) => { + setGroupName(event.target.value); }); const resetName = useEventCallback(() => { diff --git a/src/components/Collection/Group/EditGroupTabs/SeriesTab.tsx b/src/components/Collection/Group/EditGroupTabs/SeriesTab.tsx index 330850e15..02b28e958 100644 --- a/src/components/Collection/Group/EditGroupTabs/SeriesTab.tsx +++ b/src/components/Collection/Group/EditGroupTabs/SeriesTab.tsx @@ -27,7 +27,10 @@ const SeriesTab = React.memo(({ groupId }: Props) => { isSuccess: seriesSuccess, } = useGroupSeriesQuery(groupId); - const sortedSeriesData = useMemo(() => seriesData?.sort((a, b) => (a.IDs.ID > b.IDs.ID ? 1 : -1)), [seriesData]); + const sortedSeriesData = useMemo( + () => seriesData?.sort((seriesA, seriesB) => (seriesA.IDs.ID > seriesB.IDs.ID ? 1 : -1)), + [seriesData], + ); const { mutate: moveToNewGroupMutation } = useCreateGroupMutation(); const { mutate: setGroupMainSeriesMutation } = usePatchGroupMutation(); diff --git a/src/components/Collection/Series/EditSeriesTabs/GroupTab.tsx b/src/components/Collection/Series/EditSeriesTabs/GroupTab.tsx index bbbf0de32..8e0b4d0cb 100644 --- a/src/components/Collection/Series/EditSeriesTabs/GroupTab.tsx +++ b/src/components/Collection/Series/EditSeriesTabs/GroupTab.tsx @@ -156,7 +156,7 @@ function GroupTab({ seriesId }: Props) { const [search, setSearch] = useState(''); const [debouncedSearch] = useDebounceValue(search, 200); - const updateSearch = useEventCallback((e: React.ChangeEvent) => setSearch(e.target.value)); + const updateSearch = useEventCallback((event: React.ChangeEvent) => setSearch(event.target.value)); const { data: seriesGroup, isFetching } = useSeriesGroupQuery(seriesId, false); const groupsQuery = useFilteredGroupsInfiniteQuery({ diff --git a/src/components/Collection/Series/EditSeriesTabs/NameTab.tsx b/src/components/Collection/Series/EditSeriesTabs/NameTab.tsx index 93273b581..9a1a14820 100644 --- a/src/components/Collection/Series/EditSeriesTabs/NameTab.tsx +++ b/src/components/Collection/Series/EditSeriesTabs/NameTab.tsx @@ -82,7 +82,7 @@ const NameTab = ({ seriesId }: Props) => { setName(e.target.value)} + onChange={event => setName(event.target.value)} value={name} placeholder={isFetching ? 'Loading...' : undefined} label="Name" diff --git a/src/components/Collection/Series/EditSeriesTabs/PersonalStats.tsx b/src/components/Collection/Series/EditSeriesTabs/PersonalStats.tsx deleted file mode 100644 index fa7f8df94..000000000 --- a/src/components/Collection/Series/EditSeriesTabs/PersonalStats.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; - -import Checkbox from '@/components/Input/Checkbox'; -import InputSmall from '@/components/Input/InputSmall'; -import SelectSmall from '@/components/Input/SelectSmall'; - -// TODO: This one is mocked, we need to get the data from the API -function PersonalStats() { - const watchedState = { - episodes: { - current: 5, - total: 10, - }, - specials: { - current: 4, - total: 10, - }, - }; - - const seriesScore = { - isGlobal: true, - anidb: 5, - anilist: 4, - animeshon: 2, - mal: 4, - }; - - return ( -
-
-
Watched State
-
-
- Episodes -
- {}} - value={watchedState.episodes.current} - suffixes={ -
- / - {watchedState.episodes.total} -
- } - /> -
-
-
- Specials -
- {}} - value={watchedState.specials.current} - suffixes={ -
- / - {watchedState.specials.total} -
- } - /> -
-
-
-
- -
-
Series Score
-
- {}} - /> -
- AniDB - {}} - > - {[...Array(11).keys()].map(i => )} - -
-
- AniList - {}} - > - {[...Array(11).keys()].map(i => )} - -
-
- Animeshon - {}} - > - - - - -
-
- My Anime List - {}} - > - {[...Array(11).keys()].map(i => )} - -
-
-
-
- ); -} - -export default PersonalStats; diff --git a/src/components/Collection/TitleOptions.tsx b/src/components/Collection/TitleOptions.tsx index 71a891695..3f64a558e 100644 --- a/src/components/Collection/TitleOptions.tsx +++ b/src/components/Collection/TitleOptions.tsx @@ -66,7 +66,7 @@ const TitleOptions = (props: Props) => { placeholder="Search..." startIcon={mdiMagnify} value={isSeries ? seriesSearch : groupSearch} - onChange={e => setSearch(e.target.value)} + onChange={event => setSearch(event.target.value)} /> {!isSeries && ( <> diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 77c79862a..7fab2b002 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -41,8 +41,7 @@ const ErrorBoundary = ({ error, resetError }: { error?: Error, resetError?: () = const handleStableWebUiUpdate = useEventCallback(() => handleWebUiUpdate('Stable')); const handleDevWebUiUpdate = useEventCallback(() => handleWebUiUpdate('Dev')); - // eslint-disable-next-line no-console - console.log(error, routeError); + console.error(error, routeError); return (
diff --git a/src/components/Panels/ModalPanel.tsx b/src/components/Panels/ModalPanel.tsx index 4a5c90826..bbb05d374 100644 --- a/src/components/Panels/ModalPanel.tsx +++ b/src/components/Panels/ModalPanel.tsx @@ -60,7 +60,7 @@ function ModalPanel(props: Props) { fullHeight ? 'h-[75%]' : 'max-h-[75%]', className, )} - onClick={e => e.stopPropagation()} + onClick={event => event.stopPropagation()} >
{header && ( diff --git a/src/components/Settings/AvatarEditorModal.tsx b/src/components/Settings/AvatarEditorModal.tsx index 07474b437..4a7d0bbc5 100644 --- a/src/components/Settings/AvatarEditorModal.tsx +++ b/src/components/Settings/AvatarEditorModal.tsx @@ -75,7 +75,7 @@ const AvatarEditorModal = (props: Props) => { max="2" step="0.01" value={scale} - onChange={e => setScale(Number(e.target.value))} + onChange={event => setScale(Number(event.target.value))} className="grow" /> diff --git a/src/components/Utilities/FilesSummary.tsx b/src/components/Utilities/FilesSummary.tsx index 0be51b6cb..dd69269e2 100644 --- a/src/components/Utilities/FilesSummary.tsx +++ b/src/components/Utilities/FilesSummary.tsx @@ -20,10 +20,12 @@ const FilesSummary = ({ items, title }: Props) => { } const selectedSeriesCount = Object.keys( - groupBy(items.flatMap(x => x.SeriesIDs).map(x => x?.SeriesID), x => x?.ID), + groupBy(items.flatMap(file => file.SeriesIDs).map(xref => xref?.SeriesID), seriesIds => seriesIds?.ID), ).length; - const selectedSize = items.reduce((prev, cur) => prev + cur.Size, 0); - const selectedEpisodeCount = items.flatMap(x => x.SeriesIDs).map(x => x?.EpisodeIDs.flatMap(z => z.ID)).length; + const selectedSize = items.reduce((prev, current) => prev + current.Size, 0); + const selectedEpisodeCount = items + .flatMap(file => file.SeriesIDs) + .map(xref => xref?.EpisodeIDs.flatMap(episodeIDs => episodeIDs.ID)).length; return { seriesCount: selectedSeriesCount, totalSize: selectedSize, diff --git a/src/components/Utilities/ReleaseManagement/MultiplesUtilList.tsx b/src/components/Utilities/ReleaseManagement/MultiplesUtilList.tsx index 0655b93d1..15b58dcba 100644 --- a/src/components/Utilities/ReleaseManagement/MultiplesUtilList.tsx +++ b/src/components/Utilities/ReleaseManagement/MultiplesUtilList.tsx @@ -33,8 +33,8 @@ const seriesColumns: UtilityHeaderType[] = [ rel="noreferrer noopener" className="cursor-pointer text-panel-text-primary" aria-label="Open AniDB series page" - onClick={e => - e.stopPropagation()} + onClick={event => + event.stopPropagation()} > @@ -85,7 +85,7 @@ const episodeColumns: UtilityHeaderType[] = [ rel="noreferrer noopener" className="cursor-pointer text-panel-text-primary" aria-label="Open AniDB episode page" - onClick={e => e.stopPropagation()} + onClick={event => event.stopPropagation()} > diff --git a/src/components/Utilities/Renamer/AddFilesModal.tsx b/src/components/Utilities/Renamer/AddFilesModal.tsx index aad19af85..5048ea2b8 100644 --- a/src/components/Utilities/Renamer/AddFilesModal.tsx +++ b/src/components/Utilities/Renamer/AddFilesModal.tsx @@ -126,7 +126,7 @@ const AddFilesModal = ({ onClose, show }: Props) => { id="pageSize" type="number" value={pageSize} - onChange={e => setPageSize(toNumber(e.target.value))} + onChange={event => setPageSize(toNumber(event.target.value))} className="w-12 text-center" /> - )} - - )} - - {extra && ( - <> -
- {onIconClick && ( - - )} - +
+ {getEpisodePrefixAlt(episode.AniDB?.Type)} +
+
{padNumber(episode.AniDB?.EpisodeNumber ?? 0)}
+
{episode.Name}
+ {onIconClick && ( + )}
)); diff --git a/src/components/Collection/Tmdb/EpisodeRow.tsx b/src/components/Collection/Tmdb/EpisodeRow.tsx index 8b6ea4a78..d10bae499 100644 --- a/src/components/Collection/Tmdb/EpisodeRow.tsx +++ b/src/components/Collection/Tmdb/EpisodeRow.tsx @@ -2,13 +2,12 @@ import React, { useMemo } from 'react'; import { mdiLoading } from '@mdi/js'; import { Icon } from '@mdi/react'; import cx from 'classnames'; -import { find } from 'lodash'; +import { find, map } from 'lodash'; import AniDBEpisode from '@/components/Collection/Tmdb/AniDBEpisode'; import EpisodeSelect from '@/components/Collection/Tmdb/EpisodeSelect'; import MatchRating from '@/components/Collection/Tmdb/MatchRating'; import { useTmdbBulkEpisodesQuery } from '@/core/react-query/tmdb/queries'; -import { MatchRatingType } from '@/core/types/api/episode'; import useEventCallback from '@/hooks/useEventCallback'; import type { EpisodeType } from '@/core/types/api/episode'; @@ -19,11 +18,9 @@ type Props = { episode: EpisodeType; isOdd: boolean; offset: number; - overrides: Record; - setLinkOverrides: Updater>; + setLinkOverrides: Updater>; tmdbEpisodesPending: boolean; - xref?: TmdbEpisodeXrefType; - xrefsPending: boolean; + xrefs?: Record; }; const EpisodeRow = React.memo((props: Props) => { @@ -31,13 +28,19 @@ const EpisodeRow = React.memo((props: Props) => { episode, isOdd, offset, - overrides, setLinkOverrides, tmdbEpisodesPending, - xref, - xrefsPending, + xrefs, } = props; + const xref = useMemo( + () => { + if (!xrefs?.[episode.IDs.AniDB]) return undefined; + return xrefs[episode.IDs.AniDB][offset]; + }, + [episode.IDs.AniDB, offset, xrefs], + ); + // This does not actually query the server. We already queried it in the parent component // This just gets the data from the cache const tmdbEpisodesQuery = useTmdbBulkEpisodesQuery({ IDs: [] }); @@ -49,49 +52,47 @@ const EpisodeRow = React.memo((props: Props) => { const isPending = useMemo( () => { // Xrefs are not loaded yet - if (xrefsPending) return true; + if (!xrefs) return true; // Xrefs are loaded but episode doesn't have an xref if (!xref) return false; return !tmdbEpisode && tmdbEpisodesPending; }, - [tmdbEpisode, tmdbEpisodesPending, xref, xrefsPending], + [tmdbEpisode, tmdbEpisodesPending, xref, xrefs], ); - const addLink = useEventCallback(() => { - const anidbEpisodeId = episode.IDs.AniDB; + const editExtraEpisodeLink = useEventCallback(() => { + const episodeId = episode.IDs.AniDB; setLinkOverrides((draftState) => { - if (!draftState[anidbEpisodeId]) draftState[anidbEpisodeId] = [undefined]; - draftState[anidbEpisodeId]![draftState[anidbEpisodeId]!.length] = undefined; - }); - }); + if (!draftState[episodeId]) { + draftState[episodeId] = map(xrefs?.[episodeId], item => item.TmdbEpisodeID); + } - const removeLink = useEventCallback(() => { - setLinkOverrides((draftState) => { - if (!draftState[episode.IDs.AniDB]) draftState[episode.IDs.AniDB] = [undefined]; - draftState[episode.IDs.AniDB]!.splice(offset, 1); + // If index is 0, we are removing a link + if (offset === 0) draftState[episodeId].push(0); + else draftState[episodeId].splice(offset, 1); }); }); const overrideLink = useEventCallback((newTmdbId?: number) => { - const anidbEpisodeId = episode.IDs.AniDB; + const episodeId = episode.IDs.AniDB; setLinkOverrides((draftState) => { - if (!draftState[anidbEpisodeId]) draftState[anidbEpisodeId] = [undefined]; - if ( - newTmdbId === null || newTmdbId === undefined - || (newTmdbId === tmdbEpisode?.ID && draftState[anidbEpisodeId]![offset] !== undefined) - ) { - draftState[anidbEpisodeId]![offset] = undefined; - } else draftState[anidbEpisodeId]![offset] = newTmdbId; + if (!draftState[episodeId]) { + draftState[episodeId] = map(xrefs?.[episodeId], item => item.TmdbEpisodeID); + } + + if (newTmdbId === undefined) { + delete draftState[episodeId][offset]; + } else { + draftState[episodeId][offset] = newTmdbId; + } }); }); const matchRating = useMemo(() => { if (isPending) return undefined; - if (overrides[episode.IDs.AniDB]?.[offset] === 0) return undefined; - if (overrides[episode.IDs.AniDB]?.[offset]) return MatchRatingType.UserVerified; return xref?.Rating; - }, [episode, isPending, overrides, xref, offset]); + }, [isPending, xref]); return ( <> @@ -99,7 +100,7 @@ const EpisodeRow = React.memo((props: Props) => { episode={episode} isOdd={isOdd} extra={offset > 0} - onIconClick={offset === 0 ? addLink : removeLink} + onIconClick={editExtraEpisodeLink} /> @@ -107,7 +108,7 @@ const EpisodeRow = React.memo((props: Props) => { {!isPending && ( diff --git a/src/components/Collection/Tmdb/EpisodeSelect.tsx b/src/components/Collection/Tmdb/EpisodeSelect.tsx index 072a5ed55..a3db03603 100644 --- a/src/components/Collection/Tmdb/EpisodeSelect.tsx +++ b/src/components/Collection/Tmdb/EpisodeSelect.tsx @@ -19,7 +19,7 @@ import type { TmdbEpisodeType } from '@/core/types/api/tmdb'; type Props = { isOdd: boolean; overrideLink: (newTmdbId?: number) => void; - override: number | undefined; + override?: number; tmdbEpisode?: TmdbEpisodeType; }; @@ -40,11 +40,6 @@ const EpisodeSelect = React.memo((props: Props) => { const [tmdbEpisode, setTmdbEpisode] = useState(initialTmdbEpisode); useEffect(() => { - if (override === 0) { - setTmdbEpisode(undefined); - return; - } - if (override) { const episodeOverride = episodes.find(episode => episode.ID === override); if (episodeOverride) { @@ -57,20 +52,7 @@ const EpisodeSelect = React.memo((props: Props) => { }, [episodes, initialTmdbEpisode, override]); const handleSelect = useEventCallback((newSelectedEpisode?: TmdbEpisodeType) => { - if (!newSelectedEpisode) { - if (override === 0) { - // This resets the link to the initial state - overrideLink(initialTmdbEpisode?.ID); - setTmdbEpisode(initialTmdbEpisode); - } else { - overrideLink(initialTmdbEpisode ? 0 : undefined); - setTmdbEpisode(undefined); - } - return; - } - - overrideLink(newSelectedEpisode.ID); - setTmdbEpisode(newSelectedEpisode); + overrideLink(newSelectedEpisode?.ID ?? 0); }); const [scrollElement, setScrollElement] = useState(null); @@ -139,8 +121,8 @@ const EpisodeSelect = React.memo((props: Props) => { id="episode-search" type="text" value={searchText} - onChange={e => setSearchText(e.target.value)} - onKeyDown={e => e.stopPropagation()} + onChange={event => setSearchText(event.target.value)} + onKeyDown={event => event.stopPropagation()} placeholder="Enter Episode Title or Season/Episode Number..." inputClassName="!p-4" startIcon={mdiMagnify} diff --git a/src/components/Collection/Tmdb/MatchRating.tsx b/src/components/Collection/Tmdb/MatchRating.tsx index 1b9665ee2..1bea79af4 100644 --- a/src/components/Collection/Tmdb/MatchRating.tsx +++ b/src/components/Collection/Tmdb/MatchRating.tsx @@ -20,7 +20,7 @@ const getAbbreviation = (rating?: MatchRatingType) => { } }; -const MatchRating = ({ isOdd, rating }: { isOdd: boolean, rating?: MatchRatingType }) => ( +const MatchRating = React.memo(({ isOdd, rating }: { isOdd: boolean, rating?: MatchRatingType }) => (
{getAbbreviation(rating)}
-); +)); export default MatchRating; diff --git a/src/components/Collection/Tmdb/MovieRow.tsx b/src/components/Collection/Tmdb/MovieRow.tsx index d79dce1bc..1ad7b0e95 100644 --- a/src/components/Collection/Tmdb/MovieRow.tsx +++ b/src/components/Collection/Tmdb/MovieRow.tsx @@ -17,13 +17,19 @@ import type { Updater } from 'use-immer'; type Props = { episode: EpisodeType; isOdd: boolean; - overrides: Record; - setLinkOverrides: Updater>; + overrides: Record; + setLinkOverrides: Updater>; xrefs?: TmdbMovieXrefType[]; }; const MovieRow = React.memo((props: Props) => { - const { episode, isOdd, overrides, setLinkOverrides, xrefs } = props; + const { + episode, + isOdd, + overrides, + setLinkOverrides, + xrefs, + } = props; const [searchParams] = useSearchParams(); const tmdbId = toNumber(searchParams.get('id')); @@ -68,12 +74,11 @@ const MovieRow = React.memo((props: Props) => { setLinkOverrides((draftState) => { const anidbEpisodeId = episode.IDs.AniDB; const newTmdbId = tmdbMovie?.ID ? 0 : tmdbId; - if (!draftState[anidbEpisodeId]) draftState[anidbEpisodeId] = [undefined]; // If already linked episode was unlinked and linked again, remove override - if (draftState[anidbEpisodeId]![0] === 0 && newTmdbId === tmdbId) delete draftState[anidbEpisodeId]; + if (draftState[anidbEpisodeId]?.[0] === 0 && newTmdbId === tmdbId) delete draftState[anidbEpisodeId]; // If new link was created and removed, remove override - else if (draftState[anidbEpisodeId]![0] === tmdbId && newTmdbId === 0) delete draftState[anidbEpisodeId]; - else draftState[anidbEpisodeId]![0] = newTmdbId; + else if (draftState[anidbEpisodeId]?.[0] === tmdbId && newTmdbId === 0) delete draftState[anidbEpisodeId]; + else draftState[anidbEpisodeId] = [newTmdbId]; }); }); diff --git a/src/components/Collection/Tmdb/TmdbLinkSelectPanel.tsx b/src/components/Collection/Tmdb/TmdbLinkSelectPanel.tsx index 1f93e76a2..5e49e457d 100644 --- a/src/components/Collection/Tmdb/TmdbLinkSelectPanel.tsx +++ b/src/components/Collection/Tmdb/TmdbLinkSelectPanel.tsx @@ -139,7 +139,7 @@ const TmdbLinkSelectPanel = () => { id="link-search" type="text" value={searchText} - onChange={e => setSearchText(e.target.value)} + onChange={event => setSearchText(event.target.value)} placeholder="Enter Title or TMDB ID..." inputClassName="!p-4" startIcon={mdiMagnify} diff --git a/src/components/Collection/Tmdb/TopPanel.tsx b/src/components/Collection/Tmdb/TopPanel.tsx index ad7fce769..dd2205c3e 100644 --- a/src/components/Collection/Tmdb/TopPanel.tsx +++ b/src/components/Collection/Tmdb/TopPanel.tsx @@ -17,7 +17,7 @@ type Props = { disableCreateLink: boolean; handleCreateLink: () => void; seriesId: number; - xrefs?: Record; + xrefs?: Record; xrefsCount?: number; }; @@ -25,7 +25,10 @@ const TopPanel = (props: Props) => { const { createInProgress, disableCreateLink, handleCreateLink, seriesId, xrefs, xrefsCount } = props; const navigate = useNavigate(); - const flatXrefs = useMemo(() => (xrefs ? flatMap(xrefs, x => x) as TmdbEpisodeXrefType[] : undefined), [xrefs]); + const flatXrefs = useMemo( + () => (xrefs ? flatMap(xrefs, xref => xref) : undefined), + [xrefs], + ); const matchRatingCounts = useMemo( () => (flatXrefs ? countBy(flatXrefs, 'Rating') : {}), diff --git a/src/core/react-query/tmdb/helpers.ts b/src/core/react-query/tmdb/helpers.ts new file mode 100644 index 000000000..5c3d5fd88 --- /dev/null +++ b/src/core/react-query/tmdb/helpers.ts @@ -0,0 +1,7 @@ +import { transformListResultSimplified } from '@/core/react-query/helpers'; + +import type { ListResultType } from '@/core/types/api'; +import type { TmdbEpisodeXrefType } from '@/core/types/api/tmdb'; + +export const cleanTmdbEpisodeXrefs = (xrefs: ListResultType) => + transformListResultSimplified(xrefs).filter(xref => xref.TmdbEpisodeID !== null); diff --git a/src/core/react-query/tmdb/queries.ts b/src/core/react-query/tmdb/queries.ts index 5830ce69a..fc2a84386 100644 --- a/src/core/react-query/tmdb/queries.ts +++ b/src/core/react-query/tmdb/queries.ts @@ -4,6 +4,7 @@ import { toNumber } from 'lodash'; import { axios } from '@/core/axios'; import queryClient from '@/core/react-query/queryClient'; +import { cleanTmdbEpisodeXrefs } from '@/core/react-query/tmdb/helpers'; import type { TmdbBulkRequestType, @@ -28,13 +29,14 @@ export const useTmdbEpisodeXrefsQuery = ( params: TmdbEpisodeXrefRequestType, enabled = true, ) => - useQuery>({ + useQuery, unknown, TmdbEpisodeXrefType[]>({ queryKey: ['series', seriesId, 'tmdb', 'cross-references', 'episode', isNewLink, params], queryFn: () => axios.get( `Series/${seriesId}/TMDB/Show/CrossReferences/Episode${isNewLink ? '/Auto' : ''}`, { params }, ), + select: cleanTmdbEpisodeXrefs, enabled, }); @@ -64,7 +66,7 @@ export const useTmdbShowEpisodesQuery = (showId: number, params: TmdbShowEpisode export const useTmdbShowOrMovieQuery = (tmdbId: number, type: 'Show' | 'Movie', enabled = true) => useQuery({ - queryKey: ['series', 'tmdb', enabled ? type.toLowerCase() : 'unknown', tmdbId], + queryKey: ['series', 'tmdb', type, tmdbId], queryFn: () => axios.get(type === 'Movie' ? `Tmdb/Movie/Online/${tmdbId}` : `Tmdb/Show/${tmdbId}`), enabled, }); @@ -83,7 +85,7 @@ export const useTmdbSearchQuery = ( try { const idLookupData: TmdbSearchResultType = await axios.get(`Tmdb/${type}/Online/${query}`); finalData.push(idLookupData); - } catch (e) { + } catch (error) { // Ignore, show/movie not found on TMDB with provided ID } } diff --git a/src/core/react-query/tmdb/types.ts b/src/core/react-query/tmdb/types.ts index 2d06fe279..5e34a216c 100644 --- a/src/core/react-query/tmdb/types.ts +++ b/src/core/react-query/tmdb/types.ts @@ -38,7 +38,7 @@ export type TmdbSearchRequestType = { year?: number; } & PaginationType; -type TmdbEpisodeXrefMappingRequestType = { +export type TmdbEpisodeXrefMappingRequestType = { AniDBID: number; TmdbID: number; Replace?: boolean; diff --git a/src/pages/collection/series/TmdbLinking.tsx b/src/pages/collection/series/TmdbLinking.tsx index 6724759bb..f153728e5 100644 --- a/src/pages/collection/series/TmdbLinking.tsx +++ b/src/pages/collection/series/TmdbLinking.tsx @@ -1,11 +1,11 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useParams } from 'react-router'; import { useNavigate, useOutletContext, useSearchParams } from 'react-router-dom'; import { mdiLoading, mdiOpenInNew, mdiPencilCircleOutline } from '@mdi/js'; import { Icon } from '@mdi/react'; import { useVirtualizer } from '@tanstack/react-virtual'; import cx from 'classnames'; -import { debounce, filter, flatMap, groupBy, map, reduce, some, toNumber } from 'lodash'; +import { debounce, filter, forEach, groupBy, isEqual, map, reduce, some, toNumber } from 'lodash'; import { useImmer } from 'use-immer'; import AniDBEpisode from '@/components/Collection/Tmdb/AniDBEpisode'; @@ -29,11 +29,12 @@ import { useTmdbMovieXrefsQuery, useTmdbShowOrMovieQuery, } from '@/core/react-query/tmdb/queries'; -import { EpisodeTypeEnum } from '@/core/types/api/episode'; +import { EpisodeTypeEnum, MatchRatingType } from '@/core/types/api/episode'; import useEventCallback from '@/hooks/useEventCallback'; import useFlattenListResult from '@/hooks/useFlattenListResult'; import type { SeriesContextType } from '@/components/Collection/constants'; +import type { TmdbEpisodeXrefMappingRequestType } from '@/core/react-query/tmdb/types'; import type { TmdbEpisodeXrefType } from '@/core/types/api/tmdb'; const TmdbLinking = () => { @@ -77,7 +78,7 @@ const TmdbLinking = () => { ); const episodeXrefs = useMemo( () => (episodeXrefsQuery.data - ? groupBy(episodeXrefsQuery.data.List, 'AnidbEpisodeID') as Record + ? groupBy(episodeXrefsQuery.data, 'AnidbEpisodeID') as Record : undefined), [episodeXrefsQuery.data], ); @@ -89,7 +90,7 @@ const TmdbLinking = () => { const lastPageIds = useMemo( () => { - if (type !== 'Show' || !episodeXrefs) return []; + if (type !== 'Show' || !episodeXrefs || isEqual(episodeXrefs, {})) return []; const lastPage = episodesQuery.data?.pages.at(-1); if (!lastPage) return []; @@ -115,13 +116,12 @@ const TmdbLinking = () => { const [ linkOverrides, setLinkOverrides, - ] = useImmer({} as Record); + ] = useImmer({} as Record); - const estimateSize = useEventCallback((i: number) => { - const episode = episodes[i]; + const estimateSize = useEventCallback((index: number) => { + const episode = episodes[index]; if (!episode) return 56; // 56px is the minimum height of a loaded row. - const overrides = linkOverrides[episode.IDs.AniDB] ?? [undefined]; - return 56 * overrides.length; + return 56 * (linkOverrides[episode.IDs.AniDB]?.length || 1); }); const rowVirtualizer = useVirtualizer({ @@ -141,32 +141,53 @@ const TmdbLinking = () => { [fetchNextEpisodesPage], ); - const finalXrefs = useMemo>( + const movieXrefCount = useMemo( () => { - const tempXrefs: Record = {}; - - const movieXrefs = movieXrefsQuery.data ?? []; - movieXrefs - .filter((xref) => { - if (linkOverrides[xref.AnidbEpisodeID]?.[0] !== undefined) { - return linkOverrides[xref.AnidbEpisodeID]![0] === tmdbId; - } - return xref.TmdbMovieID === tmdbId; - }) - .forEach((xref) => { - tempXrefs[xref.AnidbEpisodeID] = xref.TmdbMovieID; - }); + if (!movieXrefsQuery.data) return 0; + + const tempXrefs: Record = Object.fromEntries( + map( + filter(movieXrefsQuery.data, xref => xref.TmdbMovieID === tmdbId), + xref => [xref.AnidbEpisodeID, xref.TmdbMovieID], + ), + ); - const finalLinkOverrides = Object.fromEntries(map(linkOverrides, (i, a) => [a, i?.[0]])); - Object.keys(finalLinkOverrides).forEach((episodeId) => { - if (linkOverrides[episodeId] === 0) delete finalLinkOverrides[episodeId]; + forEach(linkOverrides, (overrideIds, episodeId) => { + // The rule makes it unreadable.... + // eslint-disable-next-line prefer-destructuring + tempXrefs[toNumber(episodeId)] = overrideIds[0]; }); - return { ...tempXrefs, ...finalLinkOverrides }; + return Object.keys(tempXrefs).filter(key => tempXrefs[key] !== 0).length; }, [linkOverrides, movieXrefsQuery.data, tmdbId], ); + // Overrides merged with episodeXrefs + const finalEpisodeXrefs = useMemo(() => { + if (!episodeXrefs || !seriesQuery.data) return {}; + + const tempXrefs: Record = { ...episodeXrefs }; + + forEach(linkOverrides, (overrideIds, anidbEpisodeId) => { + const episodeId = toNumber(anidbEpisodeId); + tempXrefs[episodeId] = []; + forEach(overrideIds, (overrideId, index) => { + if (overrideId === 0) return; + tempXrefs[episodeId].push({ + AnidbAnimeID: seriesQuery.data.IDs.AniDB, + AnidbEpisodeID: episodeId, + TmdbShowID: tmdbId, + TmdbEpisodeID: overrideId, + Index: index, + Rating: MatchRatingType.UserVerified, + }); + }); + }); + + return tempXrefs; + }, [episodeXrefs, linkOverrides, seriesQuery.data, tmdbId]); + const { mutateAsync: addLink } = useTmdbAddLinkMutation(seriesId, type ?? 'Show'); const { mutateAsync: editEpisodeLinks } = useTmdbEditEpisodeXrefsMutation(seriesId); const { mutateAsync: deleteLink } = useDeleteTmdbLinkMutation(seriesId, type ?? 'Show'); @@ -181,17 +202,27 @@ const TmdbLinking = () => { } if (Object.keys(linkOverrides).length > 0) { - // The two commented lines below can be used if we need 1-n mappings const set = new Set(); + + const newMappings = reduce( + linkOverrides, + (result, overrides, episodeId) => { + forEach(overrides, (overrideId, index) => { + if (index > 0 && overrideId === 0) return; + result.push({ + AniDBID: toNumber(episodeId), + TmdbID: overrideId, + Replace: !set.has(episodeId) ? Boolean(set.add(episodeId)) : false, + }); + }); + return result; + }, + [] as TmdbEpisodeXrefMappingRequestType[], + ); + await editEpisodeLinks({ ResetAll: false, - Mapping: flatMap(linkOverrides, (overrideIds, episodeId) => (overrideIds - ? map(overrideIds.filter((a): a is number => a !== undefined), overrideId => ({ - AniDBID: toNumber(episodeId), - TmdbID: overrideId, - Replace: !set.has(episodeId) ? Boolean(set.add(episodeId)) : false, - })) - : [])), + Mapping: newMappings, }); } @@ -210,7 +241,7 @@ const TmdbLinking = () => { const linkGroups = reduce( linkOverrides, (result, overrideIds, episodeId) => { - if (overrideIds && some(overrideIds, a => a !== undefined)) result.create.push(toNumber(episodeId)); + if (overrideIds[0]) result.create.push(toNumber(episodeId)); else result.delete.push(toNumber(episodeId)); return result; }, @@ -218,12 +249,12 @@ const TmdbLinking = () => { ); const deleteLinkMutations = linkGroups.delete?.map( - episodeId => deleteLink({ ID: tmdbId, EpisodeID: toNumber(episodeId) }), + episodeId => deleteLink({ ID: tmdbId, EpisodeID: episodeId }), ) ?? []; await Promise.all(deleteLinkMutations); const newLinkMutations = linkGroups.create?.map( - episodeId => addLink({ ID: tmdbId, EpisodeID: toNumber(episodeId) }), + episodeId => addLink({ ID: tmdbId, EpisodeID: episodeId }), ); await Promise.all(newLinkMutations); @@ -258,19 +289,6 @@ const TmdbLinking = () => { setLinkOverrides({}); }); - useEffect(() => { - setLinkOverrides( - episodeXrefs - ? Object.fromEntries( - map( - episodeXrefs, - (xrefs, episodeId) => [episodeId, new Array(xrefs?.length ?? 1)], - ), - ) - : {}, - ); - }, [episodeXrefs, setLinkOverrides]); - return (
{ disableCreateLink={disableCreateLink} handleCreateLink={handleCreateLink} seriesId={seriesId} - xrefs={type === 'Show' ? episodeXrefs : undefined} - xrefsCount={type === 'Show' ? undefined : Object.keys(finalXrefs).length} + xrefs={type === 'Show' ? finalEpisodeXrefs : undefined} + xrefsCount={type === 'Show' ? undefined : movieXrefCount} />
{(seriesQuery.isPending || episodesQuery.isPending) && ( @@ -363,7 +381,8 @@ const TmdbLinking = () => { if (!episode && !episodesQuery.isFetchingNextPage) fetchNextPageDebounced(); - const overrides = linkOverrides[episode.IDs.AniDB] ?? [undefined]; + const overrides = linkOverrides[episode.IDs.AniDB] ?? finalEpisodeXrefs[episode.IDs.AniDB] ?? [0]; + return (
{ {episode && type === 'Show' && ( map( overrides, - (_, i) => ( + (_, index) => (
), From e3fcf71c9a4cf36a8bbbe459d72ea2ee80e30424 Mon Sep 17 00:00:00 2001 From: Harshith Mohan <26010946+harshithmohan@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:06:24 +0530 Subject: [PATCH 09/10] Add loader to tmdb episode select --- .../Collection/Tmdb/EpisodeSelect.tsx | 108 ++++++++++-------- 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/src/components/Collection/Tmdb/EpisodeSelect.tsx b/src/components/Collection/Tmdb/EpisodeSelect.tsx index a3db03603..9964f7689 100644 --- a/src/components/Collection/Tmdb/EpisodeSelect.tsx +++ b/src/components/Collection/Tmdb/EpisodeSelect.tsx @@ -133,65 +133,73 @@ const EpisodeSelect = React.memo((props: Props) => { className="h-80 w-full flex-col overflow-y-auto" ref={setScrollElement} > -
- {virtualItems.map((virtualItem) => { - const { index, key, start } = virtualItem; + {episodesQuery.isPending && ( +
+ +
+ )} + + {!episodesQuery.isPending && ( +
+ {virtualItems.map((virtualItem) => { + const { index, key, start } = virtualItem; + + const episode = index === 0 ? undefined : episodes[index - 1]; + + if (index !== 0 && !episode && !episodesQuery.isFetchingNextPage) fetchNextPageDebounced(); + + if (index !== 0 && !episode) { + return ( +
+ +
+ ); + } - const episode = index === 0 ? undefined : episodes[index - 1]; - - if (index !== 0 && !episode && !episodesQuery.isFetchingNextPage) fetchNextPageDebounced(); - - if (index !== 0 && !episode) { return ( -
- -
+
+ {!episode && 'XX'} + + {episode && (episode.SeasonNumber === 0 + ? `Special ${padNumber(episode.EpisodeNumber)}` + : `S${padNumber(episode.SeasonNumber)}E${padNumber(episode.EpisodeNumber)}`)} +
+ | +
+ {episode?.Title ?? 'Do Not Link Entry'} +
+
+ {episode?.AiredAt ?? ''} +
+ ); - } - - return ( - -
- {!episode && 'XX'} - - {episode && (episode.SeasonNumber === 0 - ? `Special ${padNumber(episode.EpisodeNumber)}` - : `S${padNumber(episode.SeasonNumber)}E${padNumber(episode.EpisodeNumber)}`)} -
- | -
- {episode?.Title ?? 'Do Not Link Entry'} -
-
- {episode?.AiredAt ?? ''} -
-
- ); - })} -
+ })} +
+ )}
From 80aa637b5104a8b4b6d567d471ba06555e2d9f90 Mon Sep 17 00:00:00 2001 From: Harshith Mohan <26010946+harshithmohan@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:08:45 +0530 Subject: [PATCH 10/10] Fix loaders in TmdbLinking page --- src/pages/collection/series/TmdbLinking.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/collection/series/TmdbLinking.tsx b/src/pages/collection/series/TmdbLinking.tsx index f153728e5..79d5af734 100644 --- a/src/pages/collection/series/TmdbLinking.tsx +++ b/src/pages/collection/series/TmdbLinking.tsx @@ -165,7 +165,7 @@ const TmdbLinking = () => { // Overrides merged with episodeXrefs const finalEpisodeXrefs = useMemo(() => { - if (!episodeXrefs || !seriesQuery.data) return {}; + if (!episodeXrefs || !seriesQuery.data) return undefined; const tempXrefs: Record = { ...episodeXrefs }; @@ -381,7 +381,7 @@ const TmdbLinking = () => { if (!episode && !episodesQuery.isFetchingNextPage) fetchNextPageDebounced(); - const overrides = linkOverrides[episode.IDs.AniDB] ?? finalEpisodeXrefs[episode.IDs.AniDB] ?? [0]; + const overrides = linkOverrides[episode.IDs.AniDB] ?? finalEpisodeXrefs?.[episode.IDs.AniDB] ?? [0]; return (