feat(tvdb): get tv seasons/episodes with tvdb
This commit is contained in:
@@ -9,7 +9,7 @@ import type {
|
||||
TmdbCompanySearchResponse,
|
||||
TmdbGenre,
|
||||
TmdbKeywordSearchResponse,
|
||||
} from '@server/api/themoviedb/interfaces';
|
||||
} from '@server/api/indexer/themoviedb/interfaces';
|
||||
import { DiscoverSliderType } from '@server/constants/discover';
|
||||
import type DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
|
||||
@@ -5,7 +5,7 @@ import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||
import type { TmdbKeyword } from '@server/api/indexer/themoviedb/interfaces';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
|
||||
import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb';
|
||||
import type { SortOptions as TMDBSortOptions } from '@server/api/indexer/themoviedb';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
|
||||
import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb';
|
||||
import type { SortOptions as TMDBSortOptions } from '@server/api/indexer/themoviedb';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
|
||||
@@ -5,7 +5,7 @@ import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||
import type { TmdbKeyword } from '@server/api/indexer/themoviedb/interfaces';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Spinner from '@app/assets/spinner.svg';
|
||||
import Tag from '@app/components/Common/Tag';
|
||||
import { RectangleStackIcon } from '@heroicons/react/24/outline';
|
||||
import type { TmdbGenre } from '@server/api/themoviedb/interfaces';
|
||||
import type { TmdbGenre } from '@server/api/indexer/themoviedb/interfaces';
|
||||
import useSWR from 'swr';
|
||||
|
||||
type GenreTagProps = {
|
||||
|
||||
@@ -9,7 +9,7 @@ import useSettings from '@app/hooks/useSettings';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||
import { ANIME_KEYWORD_ID } from '@server/api/indexer/themoviedb/constants';
|
||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type SeasonRequest from '@server/entity/SeasonRequest';
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
TmdbCompanySearchResponse,
|
||||
TmdbGenre,
|
||||
TmdbKeywordSearchResponse,
|
||||
} from '@server/api/themoviedb/interfaces';
|
||||
} from '@server/api/indexer/themoviedb/interfaces';
|
||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
||||
import type {
|
||||
|
||||
@@ -38,6 +38,11 @@ const SettingsLayout = ({ children }: SettingsLayoutProps) => {
|
||||
route: '/settings/users',
|
||||
regex: /^\/settings\/users/,
|
||||
},
|
||||
{
|
||||
text: 'Tvdb',
|
||||
route: '/settings/tvdb',
|
||||
regex: /^\/settings\/tvdb/,
|
||||
},
|
||||
settings.currentSettings.mediaServerType === MediaServerType.PLEX
|
||||
? {
|
||||
text: intl.formatMessage(messages.menuPlexSettings),
|
||||
|
||||
273
src/components/Settings/SettingsTvdb.tsx
Normal file
273
src/components/Settings/SettingsTvdb.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import CopyButton from '@app/components/Settings/CopyButton';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||
import { ArrowPathIcon } from '@heroicons/react/24/solid';
|
||||
import type { TvdbSettings } from '@server/lib/settings';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages('components.Settings', {
|
||||
general: 'General',
|
||||
settings: 'Settings',
|
||||
apikey: 'API Key',
|
||||
pin: 'PIN',
|
||||
enable: 'Enable',
|
||||
enableTip: 'Enable Tvdb (only for season and episode)',
|
||||
});
|
||||
|
||||
/*interface SettingsTvdbProps {
|
||||
onEdit: () => void;
|
||||
}*/
|
||||
|
||||
const SettingsTvdb = () => {
|
||||
const intl = useIntl();
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
|
||||
const { addToast } = useToasts();
|
||||
|
||||
const testConnection = async (apiKey: string | undefined, pin?: string) => {
|
||||
const response = await fetch('/api/v1/settings/tvdb/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ apiKey, pin }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to test Tvdb');
|
||||
}
|
||||
};
|
||||
|
||||
const saveSettings = async (values: TvdbSettings) => {
|
||||
const response = await fetch('/api/v1/settings/tvdb', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save Tvdb settings');
|
||||
}
|
||||
};
|
||||
|
||||
const { data, error } = useSWR<TvdbSettings>('/api/v1/settings/tvdb');
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
title={[
|
||||
intl.formatMessage(messages.general),
|
||||
intl.formatMessage(globalMessages.settings),
|
||||
]}
|
||||
/>
|
||||
<div className="mb-6">
|
||||
<h3 className="heading">{'Tvdb'}</h3>
|
||||
<p className="description">{'Settings for Tvdb'}</p>
|
||||
</div>
|
||||
<div className="section">
|
||||
<Formik
|
||||
initialValues={{
|
||||
apiKey: data?.apiKey,
|
||||
pin: data?.pin,
|
||||
enable: data?.use,
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
if (values.enable && values.apiKey === '') {
|
||||
addToast('Please enter an API key', { appearance: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsTesting(true);
|
||||
await testConnection(values.apiKey, values.pin);
|
||||
setIsTesting(false);
|
||||
} catch (e) {
|
||||
addToast('Tvdb connection error, check your credentials', {
|
||||
appearance: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await saveSettings({
|
||||
apiKey: values.apiKey,
|
||||
pin: values.pin,
|
||||
use: values.enable || false,
|
||||
});
|
||||
} catch (e) {
|
||||
addToast('Failed to save Tvdb settings', { appearance: 'error' });
|
||||
return;
|
||||
}
|
||||
addToast('Tvdb settings saved', { appearance: 'success' });
|
||||
}}
|
||||
>
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
values,
|
||||
setFieldValue,
|
||||
}) => {
|
||||
return (
|
||||
<Form className="section" data-testid="settings-main-form">
|
||||
<div className="form-row">
|
||||
<label htmlFor="apiKey" className="text-label">
|
||||
{intl.formatMessage(messages.apikey)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<SensitiveInput
|
||||
type="text"
|
||||
id="apiKey"
|
||||
className="rounded-l-only"
|
||||
value={values.apiKey}
|
||||
onChange={(e) => {
|
||||
setFieldValue('apiKey', e.target.value);
|
||||
}}
|
||||
/>
|
||||
<CopyButton
|
||||
textToCopy={values.apiKey ?? ''}
|
||||
key={'apikey'}
|
||||
/>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
className="input-action"
|
||||
>
|
||||
<ArrowPathIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label htmlFor="pin" className="text-label">
|
||||
{intl.formatMessage(messages.pin)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<SensitiveInput
|
||||
type="text"
|
||||
id="pin"
|
||||
className="rounded-l-only"
|
||||
value={values.pin}
|
||||
onChange={(e) => {
|
||||
values.pin = e.target.value;
|
||||
}}
|
||||
/>
|
||||
<CopyButton textToCopy={values.pin ?? ''} key={'pin'} />
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
className="input-action"
|
||||
>
|
||||
<ArrowPathIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label htmlFor="trustProxy" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.enable)}
|
||||
</span>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.enableTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="enable"
|
||||
name="enable"
|
||||
onChange={() => {
|
||||
setFieldValue('enable', !values.enable);
|
||||
addToast('Tvdb connection successful', {
|
||||
appearance: 'success',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{errors.apiKey &&
|
||||
touched.apiKey &&
|
||||
typeof errors.apiKey === 'string' && (
|
||||
<div className="error">{errors.apiKey}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
<Button
|
||||
buttonType="warning"
|
||||
type="button"
|
||||
disabled={isSubmitting || !isValid}
|
||||
onClick={async () => {
|
||||
setIsTesting(true);
|
||||
try {
|
||||
await testConnection(values.apiKey, values.pin);
|
||||
addToast('Tvdb connection successful', {
|
||||
appearance: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
addToast(
|
||||
'Tvdb connection error, check your credentials',
|
||||
{ appearance: 'error' }
|
||||
);
|
||||
}
|
||||
setIsTesting(false);
|
||||
}}
|
||||
>
|
||||
<BeakerIcon />
|
||||
<span>
|
||||
{isTesting
|
||||
? intl.formatMessage(globalMessages.testing)
|
||||
: intl.formatMessage(globalMessages.test)}
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
<Button
|
||||
buttonType="primary"
|
||||
type="submit"
|
||||
disabled={isSubmitting || !isValid}
|
||||
>
|
||||
<ArrowDownOnSquareIcon />
|
||||
<span>
|
||||
{isSubmitting
|
||||
? intl.formatMessage(globalMessages.saving)
|
||||
: intl.formatMessage(globalMessages.save)}
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsTvdb;
|
||||
@@ -14,12 +14,13 @@ const messages = defineMessages('components.TvDetails.Season', {
|
||||
type SeasonProps = {
|
||||
seasonNumber: number;
|
||||
tvId: number;
|
||||
seasonId: number;
|
||||
};
|
||||
|
||||
const Season = ({ seasonNumber, tvId }: SeasonProps) => {
|
||||
const Season = ({ seasonNumber, tvId, seasonId }: SeasonProps) => {
|
||||
const intl = useIntl();
|
||||
const { data, error } = useSWR<SeasonWithEpisodes>(
|
||||
`/api/v1/tv/${tvId}/season/${seasonNumber}`
|
||||
`/api/v1/tv/${tvId}/season/${seasonNumber}/${seasonId}`
|
||||
);
|
||||
|
||||
if (!data && !error) {
|
||||
|
||||
@@ -46,8 +46,8 @@ import {
|
||||
StarIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/solid';
|
||||
import { ANIME_KEYWORD_ID } from '@server/api/indexer/themoviedb/constants';
|
||||
import type { RTRating } from '@server/api/rating/rottentomatoes';
|
||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||
import { IssueStatus } from '@server/constants/issue';
|
||||
import {
|
||||
MediaRequestStatus,
|
||||
@@ -1078,6 +1078,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
<Season
|
||||
tvId={data.id}
|
||||
seasonNumber={season.seasonNumber}
|
||||
seasonId={season.id}
|
||||
/>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
|
||||
16
src/pages/settings/tvdb.tsx
Normal file
16
src/pages/settings/tvdb.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import SettingsLayout from '@app/components/Settings/SettingsLayout';
|
||||
import SettingsTvdb from '@app/components/Settings/SettingsTvdb';
|
||||
import useRouteGuard from '@app/hooks/useRouteGuard';
|
||||
import { Permission } from '@app/hooks/useUser';
|
||||
import type { NextPage } from 'next';
|
||||
|
||||
const TvdbSettingsPage: NextPage = () => {
|
||||
useRouteGuard(Permission.ADMIN);
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<SettingsTvdb />
|
||||
</SettingsLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default TvdbSettingsPage;
|
||||
Reference in New Issue
Block a user