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>pull/2445/head^2
parent
86dff12cde
commit
0842c233d0
@ -0,0 +1,228 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { User } from '../entity/User';
|
||||
import { TautulliSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
|
||||
export interface TautulliHistoryRecord {
|
||||
date: number;
|
||||
duration: number;
|
||||
friendly_name: string;
|
||||
full_title: string;
|
||||
grandparent_rating_key: number;
|
||||
grandparent_title: string;
|
||||
original_title: string;
|
||||
group_count: number;
|
||||
group_ids?: string;
|
||||
guid: string;
|
||||
ip_address: string;
|
||||
live: number;
|
||||
machine_id: string;
|
||||
media_index: number;
|
||||
media_type: string;
|
||||
originally_available_at: string;
|
||||
parent_media_index: number;
|
||||
parent_rating_key: number;
|
||||
parent_title: string;
|
||||
paused_counter: number;
|
||||
percent_complete: number;
|
||||
platform: string;
|
||||
product: string;
|
||||
player: string;
|
||||
rating_key: number;
|
||||
reference_id?: number;
|
||||
row_id?: number;
|
||||
session_key?: string;
|
||||
started: number;
|
||||
state?: string;
|
||||
stopped: number;
|
||||
thumb: string;
|
||||
title: string;
|
||||
transcode_decision: string;
|
||||
user: string;
|
||||
user_id: number;
|
||||
watched_status: number;
|
||||
year: number;
|
||||
}
|
||||
|
||||
interface TautulliHistoryResponse {
|
||||
response: {
|
||||
result: string;
|
||||
message?: string;
|
||||
data: {
|
||||
draw: number;
|
||||
recordsTotal: number;
|
||||
recordsFiltered: number;
|
||||
total_duration: string;
|
||||
filter_duration: string;
|
||||
data: TautulliHistoryRecord[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface TautulliWatchStats {
|
||||
query_days: number;
|
||||
total_time: number;
|
||||
total_plays: number;
|
||||
}
|
||||
|
||||
interface TautulliWatchStatsResponse {
|
||||
response: {
|
||||
result: string;
|
||||
message?: string;
|
||||
data: TautulliWatchStats[];
|
||||
};
|
||||
}
|
||||
|
||||
interface TautulliWatchUser {
|
||||
friendly_name: string;
|
||||
user_id: number;
|
||||
user_thumb: string;
|
||||
username: string;
|
||||
total_plays: number;
|
||||
total_time: number;
|
||||
}
|
||||
|
||||
interface TautulliWatchUsersResponse {
|
||||
response: {
|
||||
result: string;
|
||||
message?: string;
|
||||
data: TautulliWatchUser[];
|
||||
};
|
||||
}
|
||||
|
||||
class TautulliAPI {
|
||||
private axios: AxiosInstance;
|
||||
|
||||
constructor(settings: TautulliSettings) {
|
||||
this.axios = axios.create({
|
||||
baseURL: `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
|
||||
settings.port
|
||||
}${settings.urlBase ?? ''}`,
|
||||
params: { apikey: settings.apiKey },
|
||||
});
|
||||
}
|
||||
|
||||
public async getMediaWatchStats(
|
||||
ratingKey: string
|
||||
): Promise<TautulliWatchStats[]> {
|
||||
try {
|
||||
return (
|
||||
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||
params: {
|
||||
cmd: 'get_item_watch_time_stats',
|
||||
rating_key: ratingKey,
|
||||
grouping: 1,
|
||||
},
|
||||
})
|
||||
).data.response.data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong fetching media watch stats from Tautulli',
|
||||
{
|
||||
label: 'Tautulli API',
|
||||
errorMessage: e.message,
|
||||
ratingKey,
|
||||
}
|
||||
);
|
||||
throw new Error(
|
||||
`[Tautulli] Failed to fetch media watch stats: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMediaWatchUsers(
|
||||
ratingKey: string
|
||||
): Promise<TautulliWatchUser[]> {
|
||||
try {
|
||||
return (
|
||||
await this.axios.get<TautulliWatchUsersResponse>('/api/v2', {
|
||||
params: {
|
||||
cmd: 'get_item_user_stats',
|
||||
rating_key: ratingKey,
|
||||
grouping: 1,
|
||||
},
|
||||
})
|
||||
).data.response.data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong fetching media watch users from Tautulli',
|
||||
{
|
||||
label: 'Tautulli API',
|
||||
errorMessage: e.message,
|
||||
ratingKey,
|
||||
}
|
||||
);
|
||||
throw new Error(
|
||||
`[Tautulli] Failed to fetch media watch users: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserWatchStats(user: User): Promise<TautulliWatchStats> {
|
||||
try {
|
||||
if (!user.plexId) {
|
||||
throw new Error('User does not have an associated Plex ID');
|
||||
}
|
||||
|
||||
return (
|
||||
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||
params: {
|
||||
cmd: 'get_user_watch_time_stats',
|
||||
user_id: user.plexId,
|
||||
query_days: 0,
|
||||
grouping: 1,
|
||||
},
|
||||
})
|
||||
).data.response.data[0];
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong fetching user watch stats from Tautulli',
|
||||
{
|
||||
label: 'Tautulli API',
|
||||
errorMessage: e.message,
|
||||
user: user.displayName,
|
||||
}
|
||||
);
|
||||
throw new Error(
|
||||
`[Tautulli] Failed to fetch user watch stats: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserWatchHistory(
|
||||
user: User
|
||||
): Promise<TautulliHistoryRecord[]> {
|
||||
try {
|
||||
if (!user.plexId) {
|
||||
throw new Error('User does not have an associated Plex ID');
|
||||
}
|
||||
|
||||
return (
|
||||
await this.axios.get<TautulliHistoryResponse>('/api/v2', {
|
||||
params: {
|
||||
cmd: 'get_history',
|
||||
grouping: 1,
|
||||
order_column: 'date',
|
||||
order_dir: 'desc',
|
||||
user_id: user.plexId,
|
||||
length: 100,
|
||||
},
|
||||
})
|
||||
).data.response.data.data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong fetching user watch history from Tautulli',
|
||||
{
|
||||
label: 'Tautulli API',
|
||||
errorMessage: e.message,
|
||||
user: user.displayName,
|
||||
}
|
||||
);
|
||||
throw new Error(
|
||||
`[Tautulli] Failed to fetch user watch history: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TautulliAPI;
|
@ -1,6 +1,22 @@
|
||||
import type Media from '../../entity/Media';
|
||||
import { User } from '../../entity/User';
|
||||
import { PaginatedResponse } from './common';
|
||||
|
||||
export interface MediaResultsResponse extends PaginatedResponse {
|
||||
results: Media[];
|
||||
}
|
||||
|
||||
export interface MediaWatchDataResponse {
|
||||
data?: {
|
||||
users: User[];
|
||||
playCount: number;
|
||||
playCount7Days: number;
|
||||
playCount30Days: number;
|
||||
};
|
||||
data4k?: {
|
||||
users: User[];
|
||||
playCount: number;
|
||||
playCount7Days: number;
|
||||
playCount30Days: number;
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in new issue