mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
* refactor: switch ExternalAPI to Fetch API * fix: add missing auth token in Plex request * fix: send proper URL params * ci: try to fix format checker * ci: ci: try to fix format checker * ci: try to fix format checker * refactor: make tautulli use the ExternalAPI class * refactor: add rate limit to fetch api * refactor: add rate limit to fetch api * refactor: switch server from axios to fetch api * refactor: switch frontend from axios to fetch api * fix: switch from URL objects to strings * fix: use the right search params for ExternalAPI * fix: better log for ExternalAPI errors * feat: add retry to external API requests * fix: try to fix network errors with IPv6 * fix: imageProxy rate limit * revert: remove retry to external API requests * feat: set IPv4 first as an option * fix(jellyfinapi): add missing argument in JellyfinAPI constructor * refactor: clean the rate limit utility
286 lines
6.9 KiB
TypeScript
286 lines
6.9 KiB
TypeScript
import ExternalAPI from '@server/api/externalapi';
|
|
import type { User } from '@server/entity/User';
|
|
import type { TautulliSettings } from '@server/lib/settings';
|
|
import logger from '@server/logger';
|
|
import { uniqWith } from 'lodash';
|
|
|
|
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 extends ExternalAPI {
|
|
constructor(settings: TautulliSettings) {
|
|
super(
|
|
`${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
|
|
settings.port
|
|
}${settings.urlBase ?? ''}`,
|
|
{
|
|
apikey: settings.apiKey || '',
|
|
}
|
|
);
|
|
}
|
|
|
|
public async getInfo(): Promise<TautulliInfo> {
|
|
try {
|
|
return (
|
|
await this.get<TautulliInfoResponse>('/api/v2', {
|
|
cmd: 'get_tautulli_info',
|
|
})
|
|
).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.get<TautulliWatchStatsResponse>('/api/v2', {
|
|
cmd: 'get_item_watch_time_stats',
|
|
rating_key: ratingKey,
|
|
grouping: '1',
|
|
})
|
|
).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.get<TautulliWatchUsersResponse>('/api/v2', {
|
|
cmd: 'get_item_user_stats',
|
|
rating_key: ratingKey,
|
|
grouping: '1',
|
|
})
|
|
).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.get<TautulliWatchStatsResponse>('/api/v2', {
|
|
cmd: 'get_user_watch_time_stats',
|
|
user_id: user.plexId.toString(),
|
|
query_days: '0',
|
|
grouping: '1',
|
|
})
|
|
).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.get<TautulliHistoryResponse>('/api/v2', {
|
|
cmd: 'get_history',
|
|
grouping: '1',
|
|
order_column: 'date',
|
|
order_dir: 'desc',
|
|
user_id: user.plexId.toString(),
|
|
media_type: 'movie,episode',
|
|
length: take.toString(),
|
|
start: start.toString(),
|
|
})
|
|
).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;
|