feat: Tautulli integration (#2230)

* feat: media/user watch history data via Tautulli

* fix(frontend): only display slideover cog button if there is media to manage

* fix(lang): tweak permission denied messages

* refactor: reorder Media section in slideover

* refactor: use new Tautulli stats API

* fix(frontend): do not attempt to fetch data when user lacks req perms

* fix: remove unneccessary get_user requests

* feat(frontend): display user avatars

* feat: add external URL setting

* feat: add play counts for past week/month

* fix(lang): tweak strings

Co-authored-by: Ryan Cohen <ryan@sct.dev>
This commit is contained in:
TheCatLady
2022-01-20 05:36:59 -05:00
committed by GitHub
parent 86dff12cde
commit 0842c233d0
19 changed files with 1432 additions and 219 deletions

View File

@@ -2,15 +2,16 @@ import { ArrowCircleRightIcon } from '@heroicons/react/outline';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { defineMessages, FormattedNumber, useIntl } from 'react-intl';
import useSWR from 'swr';
import {
QuotaResponse,
UserRequestsResponse,
UserWatchDataResponse,
} from '../../../server/interfaces/api/userInterfaces';
import { MovieDetails } from '../../../server/models/Movie';
import { TvDetails } from '../../../server/models/Tv';
import { Permission, useUser } from '../../hooks/useUser';
import { Permission, UserType, useUser } from '../../hooks/useUser';
import Error from '../../pages/_error';
import ImageFader from '../Common/ImageFader';
import LoadingSpinner from '../Common/LoadingSpinner';
@@ -18,6 +19,7 @@ import PageTitle from '../Common/PageTitle';
import ProgressCircle from '../Common/ProgressCircle';
import RequestCard from '../RequestCard';
import Slider from '../Slider';
import TmdbTitleCard from '../TitleCard/TmdbTitleCard';
import ProfileHeader from './ProfileHeader';
const messages = defineMessages({
@@ -30,6 +32,7 @@ const messages = defineMessages({
pastdays: '{type} (past {days} days)',
movierequests: 'Movie Requests',
seriesrequest: 'Series Requests',
recentlywatched: 'Recently Watched',
});
type MediaTitle = MovieDetails | TvDetails;
@@ -46,10 +49,30 @@ const UserProfile: React.FC = () => {
>({});
const { data: requests, error: requestError } = useSWR<UserRequestsResponse>(
user ? `/api/v1/user/${user?.id}/requests?take=10&skip=0` : null
user &&
(user.id === currentUser?.id ||
currentHasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' }
))
? `/api/v1/user/${user?.id}/requests?take=10&skip=0`
: null
);
const { data: quota } = useSWR<QuotaResponse>(
user ? `/api/v1/user/${user.id}/quota` : null
user &&
(user.id === currentUser?.id ||
currentHasPermission(
[Permission.MANAGE_USERS, Permission.MANAGE_REQUESTS],
{ type: 'and' }
))
? `/api/v1/user/${user.id}/quota`
: null
);
const { data: watchData } = useSWR<UserWatchDataResponse>(
user?.userType === UserType.PLEX &&
(user.id === currentUser?.id || currentHasPermission(Permission.ADMIN))
? `/api/v1/user/${user.id}/watch_data`
: null
);
const updateAvailableTitles = useCallback(
@@ -95,7 +118,10 @@ const UserProfile: React.FC = () => {
<ProfileHeader user={user} />
{quota &&
(user.id === currentUser?.id ||
currentHasPermission(Permission.MANAGE_USERS)) && (
currentHasPermission(
[Permission.MANAGE_USERS, Permission.MANAGE_REQUESTS],
{ type: 'and' }
)) && (
<div className="relative z-40">
<dl className="grid grid-cols-1 gap-5 mt-5 lg:grid-cols-3">
<div className="px-4 py-5 overflow-hidden bg-gray-800 bg-opacity-50 rounded-lg shadow ring-1 ring-gray-700 sm:p-6">
@@ -103,10 +129,9 @@ const UserProfile: React.FC = () => {
{intl.formatMessage(messages.totalrequests)}
</dt>
<dd className="mt-1 text-3xl font-semibold text-white">
{intl.formatNumber(user.requestCount)}
<FormattedNumber value={user.requestCount} />
</dd>
</div>
<div
className={`px-4 py-5 overflow-hidden bg-gray-800 bg-opacity-50 rounded-lg shadow ring-1 ${
quota.movie.restricted
@@ -162,7 +187,6 @@ const UserProfile: React.FC = () => {
)}
</dd>
</div>
<div
className={`px-4 py-5 overflow-hidden bg-gray-800 bg-opacity-50 rounded-lg shadow ring-1 ${
quota.tv.restricted
@@ -253,6 +277,29 @@ const UserProfile: React.FC = () => {
/>
</>
)}
{(user.id === currentUser?.id ||
currentHasPermission(Permission.ADMIN)) &&
!!watchData?.recentlyWatched.length && (
<>
<div className="slider-header">
<div className="slider-title">
<span>{intl.formatMessage(messages.recentlywatched)}</span>
</div>
</div>
<Slider
sliderKey="media"
isLoading={!watchData}
isEmpty={!watchData?.recentlyWatched.length}
items={watchData.recentlyWatched.map((item) => (
<TmdbTitleCard
key={`media-slider-item-${item.id}`}
tmdbId={item.tmdbId}
type={item.mediaType}
/>
))}
/>
</>
)}
</>
);
};