|
|
|
import type { AxiosInstance } from 'axios';
|
|
|
|
import axios from 'axios';
|
|
|
|
import { uniqWith } from 'lodash';
|
|
|
|
import type { User } from '../entity/User';
|
|
|
|
import type { 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[];
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
interface TautulliInfo {
|
|
|
|
tautulli_install_type: string;
|
|
|
|
tautulli_version: string;
|
|
|
|
tautulli_branch: string;
|
|
|
|
tautulli_commit: string;
|
|
|
|
tautulli_platform: string;
|
|
|
|
tautulli_platform_release: string;
|
|
|
|
tautulli_platform_version: string;
|
|
|
|
tautulli_platform_linux_distro: string;
|
|
|
|
tautulli_platform_device_name: string;
|
|
|
|
tautulli_python_version: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface TautulliInfoResponse {
|
|
|
|
response: {
|
|
|
|
result: string;
|
|
|
|
message?: string;
|
|
|
|
data: TautulliInfo;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
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 getInfo(): Promise<TautulliInfo> {
|
|
|
|
try {
|
|
|
|
return (
|
|
|
|
await this.axios.get<TautulliInfoResponse>('/api/v2', {
|
|
|
|
params: { cmd: 'get_tautulli_info' },
|
|
|
|
})
|
|
|
|
).data.response.data;
|
|
|
|
} catch (e) {
|
|
|
|
logger.error('Something went wrong fetching Tautulli server info', {
|
|
|
|
label: 'Tautulli API',
|
|
|
|
errorMessage: e.message,
|
|
|
|
});
|
|
|
|
throw new Error(
|
|
|
|
`[Tautulli] Failed to fetch Tautulli server info: ${e.message}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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[]> {
|
|
|
|
let results: TautulliHistoryRecord[] = [];
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (!user.plexId) {
|
|
|
|
throw new Error('User does not have an associated Plex ID');
|
|
|
|
}
|
|
|
|
|
|
|
|
const take = 100;
|
|
|
|
let start = 0;
|
|
|
|
|
|
|
|
while (results.length < 20) {
|
|
|
|
const tautulliData = (
|
|
|
|
await this.axios.get<TautulliHistoryResponse>('/api/v2', {
|
|
|
|
params: {
|
|
|
|
cmd: 'get_history',
|
|
|
|
grouping: 1,
|
|
|
|
order_column: 'date',
|
|
|
|
order_dir: 'desc',
|
|
|
|
user_id: user.plexId,
|
|
|
|
media_type: 'movie,episode',
|
|
|
|
length: take,
|
|
|
|
start,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
).data.response.data.data;
|
|
|
|
|
|
|
|
if (!tautulliData.length) {
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
results = uniqWith(results.concat(tautulliData), (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
|
|
|
|
);
|
|
|
|
|
|
|
|
start += take;
|
|
|
|
}
|
|
|
|
|
|
|
|
return results.slice(0, 20);
|
|
|
|
} 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;
|