diff --git a/overseerr-api.yml b/overseerr-api.yml index 25e262897..fccc26bc0 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -4408,26 +4408,41 @@ paths: summary: Get the Plex watchlist. tags: - search + parameters: + - in: query + name: page + schema: + type: number + example: 1 + default: 1 responses: '200': description: Watchlist data returned content: application/json: schema: - type: array - items: - type: object - properties: - id: - type: number - example: 1 - backdrops: - type: array - items: - type: string - name: - type: string - example: Genre Name + type: object + properties: + page: + type: number + totalPages: + type: number + totalResults: + type: number + results: + type: array + items: + type: object + properties: + tmdbId: + type: number + example: 1 + ratingKey: + type: string + type: + type: string + title: + type: string /request: get: summary: Get all requests diff --git a/server/api/plextv.ts b/server/api/plextv.ts index cede458d1..0205e7ab7 100644 --- a/server/api/plextv.ts +++ b/server/api/plextv.ts @@ -114,6 +114,7 @@ interface UsersResponse { interface WatchlistResponse { MediaContainer: { + totalSize: number; Metadata: { ratingKey: string; }[]; @@ -289,7 +290,12 @@ class PlexTvAPI extends ExternalAPI { public async getWatchlist({ offset = 0, size = 20, - }: { offset?: number; size?: number } = {}): Promise { + }: { offset?: number; size?: number } = {}): Promise<{ + offset: number; + size: number; + totalSize: number; + items: PlexWatchlistItem[]; + }> { try { const response = await this.axios.get( '/library/sections/watchlist/all', @@ -330,13 +336,23 @@ class PlexTvAPI extends ExternalAPI { const filteredList = watchlistDetails.filter((detail) => detail.tmdbId); - return filteredList; + return { + offset, + size, + totalSize: response.data.MediaContainer.totalSize, + items: filteredList, + }; } catch (e) { logger.error('Failed to retrieve watchlist items', { label: 'Plex.TV Metadata API', errorMessage: e.message, }); - return []; + return { + offset, + size, + totalSize: 0, + items: [], + }; } } } diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index f150e76c0..d409bb035 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -94,7 +94,7 @@ class TheMovieDb extends ExternalAPI { nodeCache: cacheManager.getCache('tmdb').data, rateLimit: { maxRequests: 20, - maxRPS: 1, + maxRPS: 50, }, } ); diff --git a/server/interfaces/api/discoverInterfaces.ts b/server/interfaces/api/discoverInterfaces.ts index c91b9515c..20bdc494a 100644 --- a/server/interfaces/api/discoverInterfaces.ts +++ b/server/interfaces/api/discoverInterfaces.ts @@ -7,6 +7,6 @@ export interface GenreSliderItem { export interface WatchlistItem { ratingKey: string; tmdbId: number; - type: 'movie' | 'tv'; + mediaType: 'movie' | 'tv'; title: string; } diff --git a/server/routes/discover.ts b/server/routes/discover.ts index a1687a546..a01aa3f34 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -709,8 +709,19 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>( } ); -discoverRoutes.get('/watchlist', async (req, res) => { +discoverRoutes.get< + { page?: number }, + { + page: number; + totalPages: number; + totalResults: number; + results: WatchlistItem[]; + } +>('/watchlist', async (req, res) => { const userRepository = getRepository(User); + const itemsPerPage = 20; + const page = req.params.page ?? 1; + const offset = (page - 1) * itemsPerPage; const activeUser = await userRepository.findOne({ where: { id: req.user?.id }, @@ -719,14 +730,29 @@ discoverRoutes.get('/watchlist', async (req, res) => { if (!activeUser?.plexToken) { // We will just return an empty array if the user has no plex token - return res.json([]); + return res.json({ + page: 1, + totalPages: 1, + totalResults: 0, + results: [], + }); } const plexTV = new PlexTvAPI(activeUser?.plexToken); - const watchlist = await plexTV.getWatchlist(); - - return res.json(watchlist); + const watchlist = await plexTV.getWatchlist({ offset }); + + return res.json({ + page, + totalPages: Math.ceil(watchlist.size / itemsPerPage), + totalResults: watchlist.size, + results: watchlist.items.map((item) => ({ + ratingKey: item.ratingKey, + title: item.title, + mediaType: item.type, + tmdbId: item.tmdbId, + })), + }); }); export default discoverRoutes; diff --git a/src/components/Common/ListView/index.tsx b/src/components/Common/ListView/index.tsx index ff9013bb8..0c5a6e724 100644 --- a/src/components/Common/ListView/index.tsx +++ b/src/components/Common/ListView/index.tsx @@ -1,4 +1,5 @@ import { useIntl } from 'react-intl'; +import type { WatchlistItem } from '../../../../server/interfaces/api/discoverInterfaces'; import type { MovieResult, PersonResult, @@ -8,14 +9,16 @@ import useVerticalScroll from '../../../hooks/useVerticalScroll'; import globalMessages from '../../../i18n/globalMessages'; import PersonCard from '../../PersonCard'; import TitleCard from '../../TitleCard'; +import TmdbTitleCard from '../../TitleCard/TmdbTitleCard'; -interface ListViewProps { +type ListViewProps = { items?: (TvResult | MovieResult | PersonResult)[]; + plexItems?: WatchlistItem[]; isEmpty?: boolean; isLoading?: boolean; isReachingEnd?: boolean; onScrollBottom: () => void; -} +}; const ListView = ({ items, @@ -23,6 +26,7 @@ const ListView = ({ isLoading, onScrollBottom, isReachingEnd, + plexItems, }: ListViewProps) => { const intl = useIntl(); useVerticalScroll(onScrollBottom, !isLoading && !isEmpty && !isReachingEnd); @@ -34,6 +38,17 @@ const ListView = ({ )}
    + {plexItems?.map((title, index) => { + return ( +
  • + +
  • + ); + })} {items?.map((title, index) => { let titleCard: React.ReactNode; diff --git a/src/components/Discover/DiscoverWatchlist/index.tsx b/src/components/Discover/DiscoverWatchlist/index.tsx new file mode 100644 index 000000000..141bd4912 --- /dev/null +++ b/src/components/Discover/DiscoverWatchlist/index.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import ListView from '../../Common/ListView'; +import { defineMessages, useIntl } from 'react-intl'; +import Header from '../../Common/Header'; +import PageTitle from '../../Common/PageTitle'; +import useDiscover from '../../../hooks/useDiscover'; +import Error from '../../../pages/_error'; +import type { WatchlistItem } from '../../../../server/interfaces/api/discoverInterfaces'; + +const messages = defineMessages({ + discoverwatchlist: 'Plex Watchlist', +}); + +const DiscoverWatchlist = () => { + const intl = useIntl(); + + const { + isLoadingInitialData, + isEmpty, + isLoadingMore, + isReachingEnd, + titles, + fetchMore, + error, + } = useDiscover('/api/v1/discover/watchlist'); + + if (error) { + return ; + } + + const title = intl.formatMessage(messages.discoverwatchlist); + + return ( + <> + +
    +
    {title}
    +
    + 0) + } + isReachingEnd={isReachingEnd} + onScrollBottom={fetchMore} + /> + + ); +}; + +export default DiscoverWatchlist; diff --git a/src/components/Discover/index.tsx b/src/components/Discover/index.tsx index 1715e2a09..f66005556 100644 --- a/src/components/Discover/index.tsx +++ b/src/components/Discover/index.tsx @@ -26,6 +26,7 @@ const messages = defineMessages({ noRequests: 'No requests.', upcoming: 'Upcoming Movies', trending: 'Trending', + plexwatchlist: 'Plex Watchlist', }); const Discover = () => { @@ -45,9 +46,12 @@ const Discover = () => { } ); - const { data: watchlistItems, error: watchlistError } = useSWR< - WatchlistItem[] - >('/api/v1/discover/watchlist', { + const { data: watchlistItems, error: watchlistError } = useSWR<{ + page: number; + totalPages: number; + totalResults: number; + results: WatchlistItem[]; + }>('/api/v1/discover/watchlist', { revalidateOnMount: true, }); @@ -101,26 +105,31 @@ const Discover = () => { emptyMessage={intl.formatMessage(messages.noRequests)} /> {!( !!watchlistItems && !watchlistError && - watchlistItems.length === 0 + watchlistItems.results.length === 0 ) && ( ( + items={watchlistItems?.results.map((item) => ( ))} /> diff --git a/src/components/TitleCard/TmdbTitleCard.tsx b/src/components/TitleCard/TmdbTitleCard.tsx index 16cb699f8..974a07d4e 100644 --- a/src/components/TitleCard/TmdbTitleCard.tsx +++ b/src/components/TitleCard/TmdbTitleCard.tsx @@ -10,13 +10,20 @@ export interface TmdbTitleCardProps { tmdbId: number; tvdbId?: number; type: 'movie' | 'tv'; + canExpand?: boolean; } const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { return (movie as MovieDetails).title !== undefined; }; -const TmdbTitleCard = ({ id, tmdbId, tvdbId, type }: TmdbTitleCardProps) => { +const TmdbTitleCard = ({ + id, + tmdbId, + tvdbId, + type, + canExpand, +}: TmdbTitleCardProps) => { const { hasPermission } = useUser(); const { ref, inView } = useInView({ @@ -31,7 +38,7 @@ const TmdbTitleCard = ({ id, tmdbId, tvdbId, type }: TmdbTitleCardProps) => { if (!title && !error) { return (
    - +
    ); } @@ -57,6 +64,7 @@ const TmdbTitleCard = ({ id, tmdbId, tvdbId, type }: TmdbTitleCardProps) => { userScore={title.voteAverage} year={title.releaseDate} mediaType={'movie'} + canExpand={canExpand} /> ) : ( { userScore={title.voteAverage} year={title.firstAirDate} mediaType={'tv'} + canExpand={canExpand} /> ); }; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index f98abb46f..1297e8522 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -10,6 +10,7 @@ "components.Discover.DiscoverStudio.studioMovies": "{studio} Movies", "components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series", "components.Discover.DiscoverTvLanguage.languageSeries": "{language} Series", + "components.Discover.DiscoverWatchlist.discoverwatchlist": "Plex Watchlist", "components.Discover.MovieGenreList.moviegenres": "Movie Genres", "components.Discover.MovieGenreSlider.moviegenres": "Movie Genres", "components.Discover.NetworkSlider.networks": "Networks", @@ -20,6 +21,7 @@ "components.Discover.discovermovies": "Popular Movies", "components.Discover.discovertv": "Popular Series", "components.Discover.noRequests": "No requests.", + "components.Discover.plexwatchlist": "Plex Watchlist", "components.Discover.popularmovies": "Popular Movies", "components.Discover.populartv": "Popular Series", "components.Discover.recentlyAdded": "Recently Added", diff --git a/src/pages/discover/watchlist.tsx b/src/pages/discover/watchlist.tsx new file mode 100644 index 000000000..207e820a5 --- /dev/null +++ b/src/pages/discover/watchlist.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import type { NextPage } from 'next'; +import DiscoverWatchlist from '../../components/Discover/DiscoverWatchlist'; + +const WatchlistPage: NextPage = () => { + return ; +}; + +export default WatchlistPage;