- Settings modal (gear icon) lets you configure all service URLs and API keys from the dashboard; values persist to data/settings.json with process.env as fallback so existing .env.local setups keep working - Per-service Test button hits each service's status endpoint and reports the version on success - Discord webhook support: structured embeds per alert category (requesters, approval age, episode progress, watch-rate stats) sent on new/reopened alerts only — already-open alerts don't re-notify - Alert detail page restructured: prose descriptions replaced with labelled fields, episode progress bar for partial TV, watch-rate stat block, View in Radarr/Sonarr/Seerr action buttons, requester names link to Overseerr profiles, timestamps moved inline with status - Tab state is pure client state (no ?tab= in URL); router.back() used on alert detail for clean browser history Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
81 lines
1.9 KiB
TypeScript
81 lines
1.9 KiB
TypeScript
import { TautulliUser } from "@/lib/types";
|
|
import { getSettings } from "@/lib/settings";
|
|
|
|
interface TautulliRow {
|
|
friendly_name: string;
|
|
email: string;
|
|
plays: number;
|
|
duration: number;
|
|
last_seen: number | null;
|
|
}
|
|
|
|
interface TautulliResponse {
|
|
response: {
|
|
result: string;
|
|
data: {
|
|
data: TautulliRow[];
|
|
};
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns a Map<lowercaseEmail, TautulliUser>.
|
|
* Returns null if TAUTULLI_URL/TAUTULLI_API are not set.
|
|
*/
|
|
export async function buildTautulliMap(): Promise<Map<string, TautulliUser> | null> {
|
|
const { tautulli } = getSettings();
|
|
const url = tautulli.url;
|
|
const key = tautulli.apiKey;
|
|
|
|
if (!url || !key) return null;
|
|
|
|
const res = await fetch(
|
|
`${url}/api/v2?apikey=${key}&cmd=get_users_table&length=1000&order_column=friendly_name&order_dir=asc`,
|
|
{ cache: "no-store" }
|
|
);
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`Tautulli API error: ${res.status} ${res.statusText}`);
|
|
}
|
|
|
|
const json: TautulliResponse = await res.json();
|
|
|
|
if (json.response.result !== "success") {
|
|
throw new Error(`Tautulli API returned non-success result`);
|
|
}
|
|
|
|
const map = new Map<string, TautulliUser>();
|
|
|
|
for (const row of json.response.data.data) {
|
|
const user: TautulliUser = {
|
|
friendly_name: row.friendly_name,
|
|
email: row.email ?? "",
|
|
plays: row.plays ?? 0,
|
|
duration: row.duration ?? 0,
|
|
last_seen: row.last_seen ?? null,
|
|
};
|
|
|
|
if (user.email) {
|
|
map.set(user.email.toLowerCase(), user);
|
|
}
|
|
// Also index by friendly_name as fallback key
|
|
if (user.friendly_name) {
|
|
map.set(`name:${user.friendly_name.toLowerCase()}`, user);
|
|
}
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
export function lookupTautulliUser(
|
|
tautulliMap: Map<string, TautulliUser>,
|
|
email: string,
|
|
displayName: string
|
|
): TautulliUser | null {
|
|
return (
|
|
tautulliMap.get(email.toLowerCase()) ??
|
|
tautulliMap.get(`name:${displayName.toLowerCase()}`) ??
|
|
null
|
|
);
|
|
}
|