feat: watchlist sync integration

feature/watchlist-sync
Ryan Cohen 3 years ago committed by Ryan Cohen
parent 7943e0c339
commit 928cd5b999

@ -4403,6 +4403,31 @@ paths:
name:
type: string
example: Genre Name
/discover/watchlist:
get:
summary: Get the Plex watchlist.
tags:
- search
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
/request:
get:
summary: Get all requests

@ -1,9 +1,9 @@
import type { AxiosInstance } from 'axios';
import axios from 'axios';
import xml2js from 'xml2js';
import type { PlexDevice } from '../interfaces/api/plexInterfaces';
import cacheManager from '../lib/cache';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import ExternalAPI from './externalapi';
interface PlexAccountResponse {
user: PlexUser;
@ -112,20 +112,52 @@ interface UsersResponse {
};
}
class PlexTvAPI {
interface WatchlistResponse {
MediaContainer: {
Metadata: {
ratingKey: string;
}[];
};
}
interface MetadataResponse {
MediaContainer: {
Metadata: {
ratingKey: string;
type: 'movie' | 'tv';
title: string;
Guid: {
id: `imdb://tt${number}` | `tmdb://${number}` | `tvdb://${number}`;
}[];
}[];
};
}
export interface PlexWatchlistItem {
ratingKey: string;
tmdbId: number;
type: 'movie' | 'tv';
title: string;
}
class PlexTvAPI extends ExternalAPI {
private authToken: string;
private axios: AxiosInstance;
constructor(authToken: string) {
super(
'https://plex.tv',
{},
{
headers: {
'X-Plex-Token': authToken,
'Content-Type': 'application/json',
Accept: 'application/json',
},
nodeCache: cacheManager.getCache('plextv').data,
}
);
this.authToken = authToken;
this.axios = axios.create({
baseURL: 'https://plex.tv',
headers: {
'X-Plex-Token': this.authToken,
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
}
public async getDevices(): Promise<PlexDevice[]> {
@ -253,6 +285,53 @@ class PlexTvAPI {
)) as UsersResponse;
return parsedXml;
}
public async getWatchlist(): Promise<PlexWatchlistItem[]> {
try {
const response = await this.axios.get<WatchlistResponse>(
'/library/sections/watchlist/all',
{
baseURL: 'https://metadata.provider.plex.tv',
}
);
const watchlistDetails = await Promise.all(
response.data.MediaContainer.Metadata.map(async (watchlistItem) => {
const detailedResponse = await this.getRolling<MetadataResponse>(
`/library/metadata/${watchlistItem.ratingKey}`,
{
baseURL: 'https://metadata.provider.plex.tv',
}
);
const metadata = detailedResponse.MediaContainer.Metadata[0];
const tmdbString = metadata.Guid.find((guid) =>
guid.id.startsWith('tmdb')
);
return {
ratingKey: metadata.ratingKey,
// This should always be set? But I guess it also cannot be?
// We will filter out the 0's afterwards
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
title: metadata.title,
type: metadata.type,
};
})
);
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
return filteredList;
} catch (e) {
logger.error('Failed to retrieve watchlist items', {
label: 'Plex.TV Metadata API',
errorMessage: e.message,
});
return [];
}
}
}
export default PlexTvAPI;

@ -3,3 +3,10 @@ export interface GenreSliderItem {
name: string;
backdrops: string[];
}
export interface WatchlistItem {
ratingKey: string;
tmdbId: number;
type: 'movie' | 'tv';
title: string;
}

@ -6,7 +6,8 @@ export type AvailableCacheIds =
| 'sonarr'
| 'rt'
| 'github'
| 'plexguid';
| 'plexguid'
| 'plextv';
const DEFAULT_TTL = 300;
const DEFAULT_CHECK_PERIOD = 120;
@ -58,6 +59,10 @@ class CacheManager {
stdTtl: 86400 * 7, // 1 week cache
checkPeriod: 60 * 30,
}),
plextv: new Cache('plextv', 'Plex TV', {
stdTtl: 86400 * 7, // 1 week cache
checkPeriod: 60,
}),
};
public getCache(id: AvailableCacheIds): Cache {

@ -1,10 +1,15 @@
import { Router } from 'express';
import { sortBy } from 'lodash';
import { getRepository } from 'typeorm';
import PlexTvAPI from '../api/plextv';
import TheMovieDb from '../api/themoviedb';
import { MediaType } from '../constants/media';
import Media from '../entity/Media';
import type { User } from '../entity/User';
import type { GenreSliderItem } from '../interfaces/api/discoverInterfaces';
import { User } from '../entity/User';
import type {
GenreSliderItem,
WatchlistItem,
} from '../interfaces/api/discoverInterfaces';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import { mapProductionCompany } from '../models/Movie';
@ -704,4 +709,29 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
}
);
discoverRoutes.get<never, WatchlistItem[]>(
'/watchlist',
async (req, res, next) => {
const userRepository = getRepository(User);
const activeUser = await userRepository.findOne({
where: { id: req.user?.id },
select: ['id', 'plexToken'],
});
if (!activeUser?.plexToken) {
return next({
status: 500,
message: 'Must be a Plex account to use watchlist feature.',
});
}
const plexTV = new PlexTvAPI(activeUser?.plexToken);
const watchlist = await plexTV.getWatchlist();
return res.json(watchlist);
}
);
export default discoverRoutes;

@ -2,6 +2,7 @@ import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { WatchlistItem } from '../../../server/interfaces/api/discoverInterfaces';
import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaInterfaces';
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import { Permission, useUser } from '../../hooks/useUser';
@ -44,6 +45,12 @@ const Discover = () => {
}
);
const { data: watchlistItems, error: watchlistError } = useSWR<
WatchlistItem[]
>('/api/v1/discover/watchlist', {
revalidateOnMount: true,
});
return (
<>
<PageTitle title={intl.formatMessage(messages.discover)} />
@ -93,6 +100,31 @@ const Discover = () => {
placeholder={<RequestCard.Placeholder />}
emptyMessage={intl.formatMessage(messages.noRequests)}
/>
<div className="slider-header">
<div className="slider-title">
<span>Plex Watchlist</span>
</div>
</div>
{!(
!!watchlistItems &&
!watchlistError &&
watchlistItems.length === 0
) && (
<Slider
sliderKey="watchlist"
isLoading={!watchlistItems && !watchlistError}
isEmpty={
!!watchlistItems && !watchlistError && watchlistItems.length === 0
}
items={watchlistItems?.map((item) => (
<TmdbTitleCard
key={`watchlist-slider-item-${item.ratingKey}`}
tmdbId={item.tmdbId}
type={item.type}
/>
))}
/>
)}
<MediaSlider
sliderKey="trending"
title={intl.formatMessage(messages.trending)}

Loading…
Cancel
Save