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

@@ -1,8 +1,11 @@
import { Router } from 'express';
import gravatarUrl from 'gravatar-url';
import { uniqWith } from 'lodash';
import { getRepository, Not } from 'typeorm';
import PlexTvAPI from '../../api/plextv';
import TautulliAPI from '../../api/tautulli';
import { UserType } from '../../constants/user';
import Media from '../../entity/Media';
import { MediaRequest } from '../../entity/MediaRequest';
import { User } from '../../entity/User';
import { UserPushSubscription } from '../../entity/UserPushSubscription';
@@ -10,6 +13,7 @@ import {
QuotaResponse,
UserRequestsResponse,
UserResultsResponse,
UserWatchDataResponse,
} from '../../interfaces/api/userInterfaces';
import { hasPermission, Permission } from '../../lib/permissions';
import { getSettings } from '../../lib/settings';
@@ -475,7 +479,8 @@ router.get<{ id: string }, QuotaResponse>(
) {
return next({
status: 403,
message: 'You do not have permission to access this endpoint.',
message:
"You do not have permission to view this user's request limits.",
});
}
@@ -492,4 +497,82 @@ router.get<{ id: string }, QuotaResponse>(
}
);
router.get<{ id: string }, UserWatchDataResponse>(
'/:id/watch_data',
async (req, res, next) => {
if (
Number(req.params.id) !== req.user?.id &&
!req.user?.hasPermission(Permission.ADMIN)
) {
return next({
status: 403,
message:
"You do not have permission to view this user's recently watched media.",
});
}
const settings = getSettings().tautulli;
if (!settings.hostname || !settings.port || !settings.apiKey) {
return next({
status: 404,
message: 'Tautulli API not configured.',
});
}
try {
const mediaRepository = getRepository(Media);
const user = await getRepository(User).findOneOrFail({
where: { id: Number(req.params.id) },
select: ['id', 'plexId'],
});
const tautulli = new TautulliAPI(settings);
const watchStats = await tautulli.getUserWatchStats(user);
const watchHistory = await tautulli.getUserWatchHistory(user);
const media = (
await Promise.all(
uniqWith(watchHistory, (recordA, recordB) =>
recordA.grandparent_rating_key && recordB.grandparent_rating_key
? recordA.grandparent_rating_key ===
recordB.grandparent_rating_key
: recordA.parent_rating_key && recordB.parent_rating_key
? recordA.parent_rating_key === recordB.parent_rating_key
: recordA.rating_key === recordB.rating_key
)
.slice(0, 20)
.map(
async (record) =>
await mediaRepository.findOne({
where: {
ratingKey:
record.media_type === 'movie'
? record.rating_key
: record.grandparent_rating_key,
},
})
)
)
).filter((media) => !!media) as Media[];
return res.status(200).json({
recentlyWatched: media,
playCount: watchStats.total_plays,
});
} catch (e) {
logger.error('Something went wrong fetching user watch data', {
label: 'API',
errorMessage: e.message,
userId: req.params.id,
});
next({
status: 500,
message: 'Failed to fetch user watch data.',
});
}
}
);
export default router;