Skip to content

Commit

Permalink
Add seen by support to Tautulli
Browse files Browse the repository at this point in the history
  • Loading branch information
benscobie committed Sep 23, 2024
1 parent 4a22e7d commit 873bc93
Show file tree
Hide file tree
Showing 3 changed files with 334 additions and 5 deletions.
228 changes: 227 additions & 1 deletion server/src/modules/api/tautulli-api/tautulli-api.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,74 @@
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
import { SettingsService } from '../../..//modules/settings/settings.service';
import { TautulliApi } from './helpers/tautulli-api.helper';
import _ from 'lodash';

interface TautulliInfo {
machine_identifier: string;
version: string;
}

export interface TautulliUser {
user_id: number;
username: string;
}

export interface TautulliMetadata {
media_type:
| 'season'
| 'episode'
| 'movie'
| 'track'
| 'album'
| 'artist'
| 'show';
rating_key: string;
parent_rating_key: string;
grandparent_rating_key: string;
}

interface TautulliChildrenMetadata {
children_count: number;
children_list: TautulliMetadata[];
}

interface TautulliHistory {
recordsFiltered: number;
recordsTotal: number;
data: TautulliHistoryItem[];
draw: number;
filter_duration: string;
total_duration: string;
}

interface TautulliHistoryItem {
user_id: number;
user: string;
watched_status: number;
}

interface TautulliHistoryRequestOptions {
grouping?: 0 | 1;
include_activity?: 0 | 1;
user?: string;
user_id?: number;
rating_key?: number | string;
parent_rating_key?: number | string;
grandparent_rating_key?: number | string;
start_date?: string;
before?: string;
after?: string;
section_id?: number;
media_type?: 'movie' | 'episode' | 'track' | 'live';
transcode_decision?: 'direct play' | 'transcode' | 'copy';
guid?: string;
order_column?: string;
order_dir?: 'desc' | 'asc';
start?: number;
length?: number;
search?: string;
}

interface Response<T> {
response:
| {
Expand All @@ -21,6 +83,8 @@ interface Response<T> {
};
}

const MAX_PAGE_SIZE = 100;

@Injectable()
export class TautulliApiService {
api: TautulliApi;
Expand All @@ -43,7 +107,7 @@ export class TautulliApiService {
const response: Response<TautulliInfo> = await this.api.getWithoutCache(
'',
{
signal: AbortSignal.timeout(10000), // aborts request after 10 seconds
signal: AbortSignal.timeout(10000),
params: {
cmd: 'get_server_identity',
},
Expand All @@ -59,4 +123,166 @@ export class TautulliApiService {
return null;
}
}

public async getPaginatedHistory(
options?: TautulliHistoryRequestOptions,
): Promise<TautulliHistory | null> {
try {
options.length = options.length ? options.length : MAX_PAGE_SIZE;
options.start = options.start || options.start === 0 ? options.start : 0;

const response: Response<TautulliHistory> = await this.api.get('', {
params: {
cmd: 'get_history',
...options,
},
});

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

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

public async getHistory(
options?: Omit<TautulliHistoryRequestOptions, 'length' | 'start'>,
): Promise<TautulliHistoryItem[] | null> {
try {
const newOptions: TautulliHistoryRequestOptions = {
...options,
length: MAX_PAGE_SIZE,
start: 0,
};

let data = await this.getPaginatedHistory(newOptions);
const pageSize: number = MAX_PAGE_SIZE;

const totalCount: number =
data && data && data.recordsFiltered ? data.recordsFiltered : 0;
const pageCount: number = Math.ceil(totalCount / pageSize);
let currentPage = 1;

let results: TautulliHistoryItem[] = [];
results = _.unionBy(
results,
data && data.data && data.data && data.data.length ? data.data : [],
'id',
);

if (results.length < totalCount) {
while (currentPage < pageCount) {
newOptions.start = currentPage * pageSize;
data = await this.getPaginatedHistory(newOptions);

currentPage++;

results = _.unionBy(
results,
data && data.data && data.data && data.data.length ? data.data : [],
'id',
);

if (results.length === totalCount) {
break;
}
}
}

return results;
} catch (e) {
this.logger.log("Couldn't fetch Tautulli history!", {
label: 'Tautulli API',
errorMessage: e.message,
});
this.logger.debug(e);
return null;
}
}

public async getMetadata(
ratingKey: number | string,
): Promise<TautulliMetadata | null> {
try {
const response: Response<TautulliMetadata> = await this.api.get('', {
params: {
cmd: 'get_metadata',
rating_key: ratingKey,
},
});

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

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

public async getChildrenMetadata(
ratingKey: number | string,
): Promise<TautulliMetadata[] | null> {
try {
const response: Response<TautulliChildrenMetadata> = await this.api.get(
'',
{
params: {
cmd: 'get_children_metadata',
rating_key: ratingKey,
},
},
);

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

return response.response.data.children_list;
} catch (e) {
this.logger.log("Couldn't fetch Tautulli children metadata!", {
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('', {
params: {
cmd: 'get_users',
},
});

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

return response.response.data;
} catch (e) {
this.logger.log("Couldn't fetch Tautulli users!", {
label: 'Tautulli API',
errorMessage: e.message,
});
this.logger.debug(e);
return null;
}
}
}
20 changes: 18 additions & 2 deletions server/src/modules/rules/constants/rules.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export class RuleConstants {
name: 'seenBy',
humanName: '[list] Viewed by (username)',
mediaType: MediaType.MOVIE,
type: RuleType.TEXT, // returns id[]
type: RuleType.TEXT, // returns usernames []
} as Property,
{
id: 2,
Expand Down Expand Up @@ -617,7 +617,23 @@ export class RuleConstants {
id: Application.TAUTULLI,
name: 'Tautulli',
mediaType: MediaType.BOTH,
props: [],
props: [
{
id: 0,
name: 'seenBy',
humanName: '[list] Viewed by (username)',
mediaType: MediaType.MOVIE,
type: RuleType.TEXT, // returns usernames []
} as Property,
{
id: 1,
name: 'sw_allEpisodesSeenBy',
humanName: '[list] Users that saw all available episodes',
mediaType: MediaType.SHOW,
type: RuleType.TEXT, // return usernames []
showType: [EPlexDataType.SHOWS, EPlexDataType.SEASONS],
} as Property,
],
},
];
}
91 changes: 89 additions & 2 deletions server/src/modules/rules/getter/tautulli-getter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
} from '../constants/rules.constants';
import { EPlexDataType } from '../../api/plex-api/enums/plex-data-type-enum';
import _ from 'lodash';
import { TautulliApiService } from '../../api/tautulli-api/tautulli-api.service';
import {
TautulliApiService,
TautulliMetadata,
} from '../../api/tautulli-api/tautulli-api.service';

@Injectable()
export class TautulliGetterService {
Expand All @@ -21,5 +24,89 @@ export class TautulliGetterService {
).props;
}

async get(id: number, libItem: PlexLibraryItem, dataType?: EPlexDataType) {}
async get(id: number, libItem: PlexLibraryItem, dataType?: EPlexDataType) {
try {
const prop = this.appProperties.find((el) => el.id === id);
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',
});

if (history.length > 0) {
const viewers = history
.filter((x) => x.watched_status == 1)
.map((el) => el.user);

const uniqueViewers = [...new Set(viewers)];

return uniqueViewers;
} else {
return [];
}
}
case 'sw_allEpisodesSeenBy': {
const users = await this.tautulliApi.getUsers();
let seasons: TautulliMetadata[];

if (metadata.media_type !== 'season') {
seasons = await this.tautulliApi.getChildrenMetadata(
metadata.rating_key,
);
} else {
seasons = [metadata];
}

const allViewers = users.slice();
for (const season of seasons) {
const episodes = await this.tautulliApi.getChildrenMetadata(
season.rating_key,
);

for (const episode of episodes) {
const viewers = await this.tautulliApi.getHistory({
rating_key: episode.rating_key,
media_type: 'episode',
});

const arrLength = allViewers.length - 1;
allViewers
.slice()
.reverse()
.forEach((el, idx) => {
if (
!viewers?.find(
(viewEl) =>
viewEl.watched_status == 1 &&
el.user_id === viewEl.user_id,
)
) {
allViewers.splice(arrLength - idx, 1);
}
});
}
}

if (allViewers && allViewers.length > 0) {
const viewerIds = allViewers.map((el) => el.user_id);
return users
.filter((el) => viewerIds.includes(el.user_id))
.map((el) => el.username);
}

return [];
}
default: {
return null;
}
}
} catch (e) {
console.log(e);
this.logger.warn(`Tautulli-Getter - Action failed : ${e.message}`);
return undefined;
}
}
}

0 comments on commit 873bc93

Please sign in to comment.