Skip to content

Commit

Permalink
Start working on TMDB linking page (#1010)
Browse files Browse the repository at this point in the history
* Start working on TMDB linking page

* Refactor types
  • Loading branch information
harshithmohan authored Aug 22, 2024
1 parent 3098e93 commit e587bd3
Show file tree
Hide file tree
Showing 32 changed files with 682 additions and 114 deletions.
12 changes: 10 additions & 2 deletions src/components/Collection/SeriesMetadata.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { mdiCloseCircleOutline, mdiOpenInNew, mdiPencilCircleOutline, mdiPlusCircleOutline } from '@mdi/js';
import { Icon } from '@mdi/react';

Expand All @@ -14,6 +15,7 @@ type Props = {
};

const MetadataLink = ({ id, seriesId, site, type }: Props) => {
const navigate = useNavigate();
const { mutate: deleteTmdbLink } = useDeleteTmdbLinkMutation(type ?? 'Movie');
const { mutate: deleteTvdbLink } = useDeleteSeriesTvdbLinkMutation();

Expand All @@ -34,7 +36,13 @@ const MetadataLink = ({ id, seriesId, site, type }: Props) => {
}
}, [id, site, type]);

const canRemoveLink = useMemo(() => site === 'TvDB' || site === 'TMDB', [site]);
const canEditLink = useMemo(() => site === 'TMDB', [site]);
const canRemoveLink = useMemo(() => ['TMDB', 'TvDB'].includes(site), [site]);

const editLink = useEventCallback(() => {
if (!id || !type) return;
navigate(`../tmdb-linking/${type[0].toLowerCase()}${id}`);
});

const removeLink = useEventCallback(() => {
if (!id) return;
Expand Down Expand Up @@ -74,7 +82,7 @@ const MetadataLink = ({ id, seriesId, site, type }: Props) => {
{id
? (
<>
<Button disabled>
<Button disabled={!canEditLink} onClick={editLink} tooltip="Edit link">
<Icon className="text-panel-icon-action" path={mdiPencilCircleOutline} size={1} />
</Button>
<Button disabled={!canRemoveLink} onClick={removeLink} tooltip="Remove link">
Expand Down
85 changes: 85 additions & 0 deletions src/components/Collection/Tmdb/EpisodeRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { useMemo } from 'react';
import cx from 'classnames';
import { find } from 'lodash';

import MatchRating from '@/components/Collection/Tmdb/MatchRating';
import { getEpisodePrefixAlt } from '@/core/utilities/getEpisodePrefix';

import type { EpisodeType } from '@/core/types/api/episode';
import type { TmdbEpisodeXRefType, TmdbMovieXRefType } from '@/core/types/api/tmdb';

const Episode = React.memo(({ episode, xref }: { episode: EpisodeType, xref?: TmdbEpisodeXRefType }) => {
const tmdbEpisode = useMemo(() => {
if (!xref || !episode.TMDB) return undefined;
return find(episode.TMDB.Episodes, { ID: xref.TmdbEpisodeID });
}, [episode, xref]);

return (
<>
{/* eslint-disable-next-line no-nested-ternary */}
{tmdbEpisode ? (tmdbEpisode.SeasonNumber === 0 ? 'SP' : `S${tmdbEpisode.SeasonNumber}`) : 'XX'}
<div>{tmdbEpisode?.EpisodeNumber.toString().padStart(2, '0') ?? 'XX'}</div>
{tmdbEpisode?.Title ?? 'Entry Not Linked'}
</>
);
});

const Movie = React.memo(({ episode, xref }: { episode: EpisodeType, xref?: TmdbMovieXRefType }) => {
const tmdbMovie = useMemo(() => {
if (!xref || !episode.TMDB) return undefined;
return find(episode.TMDB.Movies, { ID: xref.TmdbMovieID });
}, [episode, xref]);

return (
<>
Movie
<div>
{tmdbMovie?.Title ?? 'Entry Not Linked'}
</div>
</>
);
});

type Props = {
episode: EpisodeType;
type: 'tv' | 'movie' | null;
xrefs: TmdbEpisodeXRefType[] | TmdbMovieXRefType[];
isOdd: boolean;
};

const EpisodeRow = ({ episode, isOdd, type, xrefs }: 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],
);

return (
<>
<div
className={cx(
'flex grow basis-0 gap-x-6 rounded-lg border border-panel-border p-4 leading-5',
isOdd ? 'bg-panel-background-alt' : 'bg-panel-background',
)}
>
{getEpisodePrefixAlt(episode.AniDB?.Type)}
<div>{episode.AniDB?.EpisodeNumber.toString().padStart(2, '0')}</div>
{episode.Name}
</div>

{type === 'tv' && <MatchRating rating={(xref as TmdbEpisodeXRefType | undefined)?.Rating} />}

<div
className={cx(
'flex grow basis-0 gap-x-6 rounded-lg border border-panel-border p-4 leading-5',
isOdd ? 'bg-panel-background-alt' : 'bg-panel-background',
)}
>
{type === 'tv' && <Episode episode={episode} xref={xref as TmdbEpisodeXRefType | undefined} />}
{type === 'movie' && <Movie episode={episode} xref={xref as TmdbMovieXRefType | undefined} />}
</div>
</>
);
};

export default EpisodeRow;
38 changes: 38 additions & 0 deletions src/components/Collection/Tmdb/MatchRating.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import cx from 'classnames';

import { MatchRatingType } from '@/core/types/api/episode';

const getAbbreviation = (rating?: MatchRatingType) => {
switch (rating) {
case MatchRatingType.DateAndTitleMatches:
return 'DT';
case MatchRatingType.DateMatches:
case MatchRatingType.TitleMatches:
return 'D/T';
case MatchRatingType.UserVerified:
return 'UO';
case MatchRatingType.FirstAvailable:
return 'BG';
default:
return '';
}
};

const MatchRating = ({ rating }: { rating?: MatchRatingType }) => (
<div
className={cx(
'flex justify-center items-center rounded-md w-14 text-button-primary-text',
{
'bg-panel-text-important': rating === MatchRatingType.DateAndTitleMatches,
'bg-panel-text-warning': rating === MatchRatingType.DateMatches || rating === MatchRatingType.TitleMatches,
'bg-panel-text-primary': rating === MatchRatingType.UserVerified,
'bg-panel-text-danger': rating === MatchRatingType.FirstAvailable,
},
)}
>
{getAbbreviation(rating)}
</div>
);

export default MatchRating;
74 changes: 74 additions & 0 deletions src/components/Collection/Tmdb/TopPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { mdiLinkPlus } from '@mdi/js';
import { Icon } from '@mdi/react';
import { countBy } from 'lodash';

import Button from '@/components/Input/Button';
import ShokoPanel from '@/components/Panels/ShokoPanel';
import ItemCount from '@/components/Utilities/ItemCount';

import type { MatchRatingType } from '@/core/types/api/episode';
import type { TmdbXrefType } from '@/core/types/api/tmdb';

const TopPanel = (props: { seriesId: string, xrefs: TmdbXrefType[], xrefsCount: number }) => {
const { seriesId, xrefs, xrefsCount } = props;
const navigate = useNavigate();

const matchRatingCounts = useMemo(
() => countBy(xrefs, 'Rating'),
[xrefs],
) as Record<MatchRatingType, number>;

return (
<ShokoPanel title="Metadata Linking" options={<ItemCount count={xrefsCount} suffix="Entries" />}>
<div className="flex items-center gap-x-3">
<div className="flex grow items-center gap-x-4 rounded-lg border border-panel-border bg-panel-background-alt px-4 py-3">
<div className="flex items-center gap-x-2">
<div className="rounded-md bg-panel-text-important px-2 text-button-primary-text">
{matchRatingCounts.DateAndTitleMatches ?? 0}
</div>
Dates and Title Match (DT)
</div>
<div className="flex items-center gap-x-2">
<div className="rounded-md bg-panel-text-warning px-2 text-button-primary-text">
{(matchRatingCounts.DateMatches ?? 0) + (matchRatingCounts.TitleMatches ?? 0)}
</div>
Dates or Title Match (D/T)
</div>
<div className="flex items-center gap-x-2">
<div className="rounded-md bg-panel-text-primary px-2 text-button-primary-text">
{matchRatingCounts.UserVerified ?? 0}
</div>
User Overridden (UO)
</div>
<div className="flex items-center gap-x-2">
<div className="rounded-md bg-panel-text-danger px-2 text-button-primary-text">
{matchRatingCounts.FirstAvailable ?? 0}
</div>
Best Guess (BG)
</div>
</div>
<Button
buttonType="secondary"
buttonSize="normal"
className="flex flex-row flex-wrap items-center gap-x-2 py-3"
onClick={() => navigate(`/webui/collection/series/${seriesId}`)}
>
Cancel
</Button>
<Button
buttonType="primary"
buttonSize="normal"
className="flex flex-row flex-wrap items-center gap-x-2 py-3"
onClick={() => {}}
>
<Icon path={mdiLinkPlus} size={1} />
Create Links
</Button>
</div>
</ShokoPanel>
);
};

export default TopPanel;
2 changes: 1 addition & 1 deletion src/components/Input/SelectEpisodeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import cx from 'classnames';
import { find, toInteger } from 'lodash';

import { EpisodeTypeEnum } from '@/core/types/api/episode';
import getEpisodePrefix from '@/core/utilities/getEpisodePrefix';
import { getEpisodePrefix } from '@/core/utilities/getEpisodePrefix';
import useEventCallback from '@/hooks/useEventCallback';

import Input from './Input';
Expand Down
8 changes: 4 additions & 4 deletions src/components/Utilities/ItemCount.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react';

const ItemCount = ({ count, selected, series = false }: { count: number, selected?: number, series?: boolean }) => (
const ItemCount = ({ count, selected, suffix }: { count: number, selected?: number, suffix?: string }) => (
<div className="text-lg font-semibold">
<span>
<span className="text-panel-text-important">
{count}
&nbsp;
</span>
{series && 'Series'}
{!series && (count === 1 ? 'File' : 'Files')}
{suffix && suffix}
{!suffix && (count === 1 ? 'File' : 'Files')}
</span>
{(selected ?? 0) > 0 && (
<>
Expand All @@ -18,7 +18,7 @@ const ItemCount = ({ count, selected, series = false }: { count: number, selecte
{selected ?? 0}
&nbsp;
</span>
{series && 'Series'}
{suffix && suffix}
Selected
</span>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { countBy, forEach } from 'lodash';

import FileInfo from '@/components/FileInfo';
import Select from '@/components/Input/Select';
import getEpisodePrefix from '@/core/utilities/getEpisodePrefix';
import { getEpisodePrefix } from '@/core/utilities/getEpisodePrefix';

import type { MultipleFileOptionsType } from '@/components/Utilities/constants';
import type { EpisodeType } from '@/core/types/api/episode';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
useSeriesEpisodesWithMultipleReleases,
useSeriesWithMultipleReleases,
} from '@/core/react-query/release-management/queries';
import getEpisodePrefix from '@/core/utilities/getEpisodePrefix';
import { getEpisodePrefix } from '@/core/utilities/getEpisodePrefix';
import useFlattenListResult from '@/hooks/useFlattenListResult';

import type { UtilityHeaderType } from '@/components/Utilities/constants';
Expand Down
2 changes: 1 addition & 1 deletion src/components/Utilities/Renamer/ConfigModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useRenamerNewConfigMutation, useRenamerPatchConfigMutation } from '@/co
import { useRenamerConfigsQuery, useRenamersQuery } from '@/core/react-query/renamer/queries';
import useEventCallback from '@/hooks/useEventCallback';

import type { RenamerConfigType } from '@/core/react-query/renamer/types';
import type { RenamerConfigType } from '@/core/types/api/renamer';

type Props = {
config: RenamerConfigType;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Utilities/Renamer/RenamerScript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { findKey } from 'lodash';

import useEventCallback from '@/hooks/useEventCallback';

import type { RenamerConfigSettingsType, RenamerSettingsType } from '@/core/react-query/renamer/types';
import type { RenamerConfigSettingsType, RenamerSettingsType } from '@/core/types/api/renamer';
import type { Updater } from 'use-immer';

const RenamerEditor = lazy(
Expand Down
2 changes: 1 addition & 1 deletion src/components/Utilities/Renamer/RenamerSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Checkbox from '@/components/Input/Checkbox';
import InputSmall from '@/components/Input/InputSmall';
import useEventCallback from '@/hooks/useEventCallback';

import type { RenamerConfigSettingsType, RenamerSettingsType } from '@/core/react-query/renamer/types';
import type { RenamerConfigSettingsType, RenamerSettingsType } from '@/core/types/api/renamer';
import type { Updater } from 'use-immer';

type Props = {
Expand Down
4 changes: 2 additions & 2 deletions src/core/react-query/episode/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { transformListResultSimplified } from '@/core/react-query/helpers';

import type { FileRequestType } from '@/core/react-query/types';
import type { ListResultType } from '@/core/types/api';
import type { EpisodeAniDBType } from '@/core/types/api/episode';
import type { AniDBEpisodeType } from '@/core/types/api/episode';
import type { FileType } from '@/core/types/api/file';

export const useEpisodeFilesQuery = (
Expand All @@ -21,7 +21,7 @@ export const useEpisodeFilesQuery = (
});

export const useEpisodeAniDBQuery = (episodeId: number, enabled = true) =>
useQuery<EpisodeAniDBType>({
useQuery<AniDBEpisodeType>({
queryKey: ['episode', 'anidb', episodeId],
queryFn: () => axios.get(`Episode/${episodeId}/AniDB`),
enabled,
Expand Down
8 changes: 5 additions & 3 deletions src/core/react-query/renamer/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import store from '@/core/store';

import type {
RenamerConfigResponseType,
RenamerConfigSettingsType,
RenamerConfigType,
RenamerRelocateBaseRequestType,
RenamerResponseType,
} from '@/core/react-query/renamer/types';
import type {
RenamerConfigSettingsType,
RenamerConfigType,
RenamerResultType,
RenamerSettingsType,
RenamerType,
} from '@/core/react-query/renamer/types';
} from '@/core/types/api/renamer';

export const updateResults = (response: RenamerResultType[]) => {
const mappedResults = response.reduce(
Expand Down
3 changes: 1 addition & 2 deletions src/core/react-query/renamer/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import { updateApiErrors, updateResults } from '@/core/react-query/renamer/helpe

import type {
RenamerConfigResponseType,
RenamerConfigType,
RenamerPatchRequestType,
RenamerPreviewRequestType,
RenamerRelocateRequestType,
RenamerResultType,
} from '@/core/react-query/renamer/types';
import type { RenamerConfigType, RenamerResultType } from '@/core/types/api/renamer';

export const useRenamerPreviewMutation = () =>
useMutation<RenamerResultType[], unknown, RenamerPreviewRequestType>({
Expand Down
8 changes: 2 additions & 6 deletions src/core/react-query/renamer/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ import { useQuery } from '@tanstack/react-query';
import { axios } from '@/core/axios';
import { transformRenamer, transformRenamerConfigs } from '@/core/react-query/renamer/helpers';

import type {
RenamerConfigResponseType,
RenamerConfigType,
RenamerResponseType,
RenamerType,
} from '@/core/react-query/renamer/types';
import type { RenamerConfigResponseType, RenamerResponseType } from '@/core/react-query/renamer/types';
import type { RenamerConfigType, RenamerType } from '@/core/types/api/renamer';

export const useRenamersQuery = (enabled = true) =>
useQuery<RenamerResponseType[]>({
Expand Down
Loading

0 comments on commit e587bd3

Please sign in to comment.