Skip to content

Commit

Permalink
feat: Add additional Tautulli rules
Browse files Browse the repository at this point in the history
  • Loading branch information
benscobie committed Sep 26, 2024
1 parent 55aa547 commit 353ba13
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 10 deletions.
59 changes: 54 additions & 5 deletions server/src/modules/api/tautulli-api/tautulli-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface TautulliMetadata {
rating_key: string;
parent_rating_key: string;
grandparent_rating_key: string;
added_at: string;
}

interface TautulliChildrenMetadata {
Expand All @@ -45,9 +46,13 @@ interface TautulliHistoryItem {
user_id: number;
user: string;
watched_status: number;
stopped: number;
rating_key: number;
media_index: number;
parent_media_index: number;
}

interface TautulliHistoryRequestOptions {
export interface TautulliHistoryRequestOptions {
grouping?: 0 | 1;
include_activity?: 0 | 1;
user?: string;
Expand All @@ -69,6 +74,17 @@ interface TautulliHistoryRequestOptions {
search?: string;
}

interface TautulliItemWatchTimeStatsRequestOptions {
grouping?: 0 | 1;
rating_key: number | string;
}

interface TautulliItemWatchTimeStats {
query_days: 1 | 7 | 30 | 0;
total_time: number;
total_plays: number;
}

interface Response<T> {
response:
| {
Expand Down Expand Up @@ -139,12 +155,14 @@ export class TautulliApiService {
});

if (response.response.result !== 'success') {
throw new Error('Non-success response when fetching Tautulli users');
throw new Error(
'Non-success response when fetching Tautulli paginated history',
);
}

return response.response.data;
} catch (e) {
this.logger.log("Couldn't fetch Tautulli history!", {
this.logger.log("Couldn't fetch Tautulli paginated history!", {
label: 'Tautulli API',
errorMessage: e.message,
});
Expand Down Expand Up @@ -220,7 +238,7 @@ export class TautulliApiService {
});

if (response.response.result !== 'success') {
throw new Error('Non-success response when fetching Tautulli users');
throw new Error('Non-success response when fetching Tautulli metadata');
}

return response.response.data;
Expand Down Expand Up @@ -249,7 +267,9 @@ export class TautulliApiService {
);

if (response.response.result !== 'success') {
throw new Error('Non-success response when fetching Tautulli users');
throw new Error(
'Non-success response when fetching Tautulli children metadata',
);
}

return response.response.data.children_list;
Expand All @@ -263,6 +283,35 @@ export class TautulliApiService {
}
}

public async getItemWatchTimeStats(
options: TautulliItemWatchTimeStatsRequestOptions,
): Promise<TautulliItemWatchTimeStats[] | null> {
try {
const response: Response<TautulliItemWatchTimeStats[]> =
await this.api.get('', {
params: {
cmd: 'get_item_watch_time_stats',
...options,
},
});

if (response.response.result !== 'success') {
throw new Error(
'Non-success response when fetching Tautulli item watch time stats',
);
}

return response.response.data;
} catch (e) {
this.logger.log("Couldn't fetch Tautulli item watch time stats!", {
label: 'Tautulli API',
errorMessage: e.message,
});
this.logger.debug(e);
return null;
}
}

public async getUsers(): Promise<TautulliUser[] | null> {
try {
const response: Response<TautulliUser[]> = await this.api.get('', {
Expand Down
56 changes: 56 additions & 0 deletions server/src/modules/rules/constants/rules.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,62 @@ export class RuleConstants {
type: RuleType.TEXT, // return usernames []
showType: [EPlexDataType.SHOWS, EPlexDataType.SEASONS],
} as Property,
{
id: 2,
name: 'addDate',
humanName: 'Date added',
mediaType: MediaType.BOTH,
type: RuleType.DATE,
} as Property,
{
id: 3,
name: 'viewCount',
humanName: 'Times viewed',
mediaType: MediaType.MOVIE,
type: RuleType.NUMBER,
} as Property,
{
id: 4,
name: 'lastViewedAt',
humanName: 'Last view date',
mediaType: MediaType.BOTH,
type: RuleType.DATE,
} as Property,
{
id: 5,
name: 'sw_amountOfViews',
humanName: 'Total views',
mediaType: MediaType.SHOW,
type: RuleType.NUMBER,
} as Property,
{
id: 6,
name: 'sw_viewedEpisodes',
humanName: 'Amount of watched episodes',
mediaType: MediaType.SHOW,
type: RuleType.NUMBER,
showType: [EPlexDataType.SHOWS, EPlexDataType.SEASONS],
} as Property,
{
id: 7,
name: 'sw_lastWatched',
humanName: 'Newest episode view date',
mediaType: MediaType.SHOW,
type: RuleType.DATE,
showType: [EPlexDataType.SHOWS, EPlexDataType.SEASONS],
} as Property,
{
id: 8,
name: 'sw_watchers',
humanName: '[list] Users that watch the show/season/episode',
mediaType: MediaType.SHOW,
type: RuleType.TEXT, // return usernames []
showType: [
EPlexDataType.SHOWS,
EPlexDataType.SEASONS,
EPlexDataType.EPISODES,
],
} as Property,
],
},
];
Expand Down
103 changes: 98 additions & 5 deletions server/src/modules/rules/getter/tautulli-getter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { EPlexDataType } from '../../api/plex-api/enums/plex-data-type-enum';
import _ from 'lodash';
import {
TautulliApiService,
TautulliHistoryRequestOptions,
TautulliMetadata,
} from '../../api/tautulli-api/tautulli-api.service';

Expand All @@ -30,11 +31,22 @@ export class TautulliGetterService {
const metadata = await this.tautulliApi.getMetadata(libItem.ratingKey);

switch (prop.name) {
case 'seenBy': {
const history = await this.tautulliApi.getHistory({
rating_key: metadata.rating_key,
media_type: 'movie',
});
case 'seenBy':
case 'sw_watchers': {
const options: TautulliHistoryRequestOptions = {};

if (
metadata.media_type == 'episode' ||
metadata.media_type == 'movie'
) {
options.rating_key = metadata.rating_key;
} else if (metadata.media_type == 'season') {
options.parent_rating_key = metadata.rating_key;
} else {
options.grandparent_rating_key = metadata.rating_key;
}

const history = await this.tautulliApi.getHistory(options);

if (history.length > 0) {
const viewers = history
Expand Down Expand Up @@ -99,6 +111,87 @@ export class TautulliGetterService {

return [];
}
case 'addDate': {
return new Date(+metadata.added_at * 1000);
}
case 'viewCount':
case 'sw_amountOfViews': {
const itemWatchTimeStats =
await this.tautulliApi.getItemWatchTimeStats({
rating_key: metadata.rating_key,
grouping: 1,
});

return itemWatchTimeStats.find((x) => x.query_days == 0).total_plays;
}
case 'lastViewedAt': {
// get_metadata has a last_viewed_at field which would be easier but it's not correct
const options: TautulliHistoryRequestOptions = {};

if (
metadata.media_type == 'movie' ||
metadata.media_type == 'episode'
) {
options.rating_key = metadata.rating_key;
} else if (metadata.media_type == 'season') {
options.parent_rating_key = metadata.rating_key;
} else {
options.grandparent_rating_key = metadata.rating_key;
}

const history = await this.tautulliApi.getHistory(options);
const sortedHistory = history
.filter((x) => x.watched_status == 1)
.map((el) => el.stopped)
.sort()
.reverse();

return sortedHistory.length > 0
? new Date(sortedHistory[0] * 1000)
: null;
}
case 'sw_viewedEpisodes': {
const history =
metadata.media_type !== 'season'
? await this.tautulliApi.getHistory({
grandparent_rating_key: metadata.rating_key,
})
: await this.tautulliApi.getHistory({
parent_rating_key: metadata.rating_key,
});

const watchedEpisodes = history
.filter((x) => x.watched_status == 1)
.map((x) => x.rating_key);

const uniqueEpisodes = [...new Set(watchedEpisodes)];

return uniqueEpisodes.length;
}
case 'sw_lastWatched': {
let history =
metadata.media_type !== 'season'
? await this.tautulliApi.getHistory({
grandparent_rating_key: metadata.rating_key,
})
: await this.tautulliApi.getHistory({
parent_rating_key: metadata.rating_key,
});

history
.filter((x) => x.watched_status == 1)
.sort((a, b) => a.parent_media_index - b.parent_media_index)
.reverse();

history = history.filter(
(el) => el.parent_media_index === history[0].parent_media_index,
);
history.sort((a, b) => a.media_index - b.media_index).reverse();

return history.length > 0
? new Date(history[0].stopped * 1000)
: null;
}
default: {
return null;
}
Expand Down

0 comments on commit 353ba13

Please sign in to comment.