mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 04:08:45 -05:00
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:
@@ -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}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user