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 type Media from '../../entity/Media';
|
||||||
|
import { User } from '../../entity/User';
|
||||||
import { PaginatedResponse } from './common';
|
import { PaginatedResponse } from './common';
|
||||||
|
|
||||||
export interface MediaResultsResponse extends PaginatedResponse {
|
export interface MediaResultsResponse extends PaginatedResponse {
|
||||||
results: Media[];
|
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