diff --git a/src/app/dashboard/users/page.tsx b/src/app/dashboard/users/page.tsx index 7973a767..f7af27aa 100644 --- a/src/app/dashboard/users/page.tsx +++ b/src/app/dashboard/users/page.tsx @@ -10,6 +10,7 @@ import fetchTautulli, { getLibrariesByType } from '@/utils/fetchTautulli' import { secondsToTime, timeToSeconds } from '@/utils/formatting' import getPeriod from '@/utils/getPeriod' import getSettings from '@/utils/getSettings' +import { anonymizeUsers } from '@/utils/helpers' import { Metadata } from 'next' import { getServerSession } from 'next-auth' import { notFound } from 'next/navigation' @@ -51,7 +52,7 @@ async function getUsers( const allUsersCount = await getUsersCount(settings) if (!allUsersCount) { - console.error('[TAUTULLI] - Could not determine the number of users.') + console.error('Could not determine the number of users!') return } @@ -165,6 +166,10 @@ async function getUsers( user.audio_plays_count = usersPlaysAndDurations[i].audio_plays_count }) + if (settings.general.isAnonymized) { + return anonymizeUsers(listedUsers, loggedInUserId) + } + return listedUsers } diff --git a/src/app/settings/general/_actions/updateGeneralSettings.ts b/src/app/settings/general/_actions/updateGeneralSettings.ts index ebb44115..e4896f95 100644 --- a/src/app/settings/general/_actions/updateGeneralSettings.ts +++ b/src/app/settings/general/_actions/updateGeneralSettings.ts @@ -9,6 +9,7 @@ const schema = z.object({ isPostersTmdbOnly: z.boolean(), googleAnalyticsId: z.string(), isOutsideAccess: z.boolean(), + isAnonymized: z.boolean(), complete: z.boolean(), }) @@ -21,6 +22,7 @@ export default async function saveGeneralSettings( isPostersTmdbOnly: formData.get('isPostersTmdbOnly') === 'on', googleAnalyticsId: formData.get('googleAnalyticsId') as string, isOutsideAccess: formData.get('isOutsideAccess') === 'on', + isAnonymized: formData.get('isAnonymized') === 'on', complete: true, } diff --git a/src/app/settings/general/_components/GeneralSettingsForm.tsx b/src/app/settings/general/_components/GeneralSettingsForm.tsx index bda91bb5..b3a3b570 100644 --- a/src/app/settings/general/_components/GeneralSettingsForm.tsx +++ b/src/app/settings/general/_components/GeneralSettingsForm.tsx @@ -120,6 +120,17 @@ export default function GeneralSettingsForm({ settings, libraries }: Props) { Access without login. + +
+ + Anonymize + Hide usernames for other users. + +

Analytics

diff --git a/src/components/MediaItem/MediaItem.tsx b/src/components/MediaItem/MediaItem.tsx index 43f041a5..5122cca9 100644 --- a/src/components/MediaItem/MediaItem.tsx +++ b/src/components/MediaItem/MediaItem.tsx @@ -18,9 +18,10 @@ import clsx from 'clsx' import { motion } from 'framer-motion' import Image from 'next/image' import { useEffect, useRef, useState } from 'react' +import anonymousSvg from './anonymous.svg' import MediaItemTitle from './MediaItemTitle' -import PlexDeeplink from './PlexDeeplink' import placeholderSvg from './placeholder.svg' +import PlexDeeplink from './PlexDeeplink' type Props = { data: TautulliItemRow @@ -53,16 +54,19 @@ export default function MediaItem({ isUserDashboard ? data.user_thumb : data.thumb }&width=300`, )}` + const isAnonymized = data.user === 'Anonymous' + const initialImageSrc = + isUserDashboard && isAnonymized ? anonymousSvg : posterSrc + const [imageSrc, setImageSrc] = useState(initialImageSrc) const [dataKey, setDataKey] = useState(0) const titleContainerRef = useRef(null) const isOverseerrActive = settings.connection.overseerrUrl && settings.connection.overseerrApiKey - const [imageSrc, setImageSrc] = useState(posterSrc) useEffect(() => { setDataKey((prevDataKey) => prevDataKey + 1) - setImageSrc(posterSrc) - }, [data, type, posterSrc]) + setImageSrc(initialImageSrc) + }, [data, type, initialImageSrc]) return ( setImageSrc(placeholderSvg)} diff --git a/src/components/MediaItem/anonymous.svg b/src/components/MediaItem/anonymous.svg new file mode 100644 index 00000000..3f99b757 --- /dev/null +++ b/src/components/MediaItem/anonymous.svg @@ -0,0 +1,8 @@ + + + + diff --git a/src/types/settings.d.ts b/src/types/settings.d.ts index cc341cb9..dfd19ca1 100644 --- a/src/types/settings.d.ts +++ b/src/types/settings.d.ts @@ -18,6 +18,7 @@ export type GeneralSettings = { isPostersTmdbOnly: boolean googleAnalyticsId: string isOutsideAccess: boolean + isAnonymized: boolean complete: boolean } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 27e11011..2f3d7ff3 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -51,6 +51,7 @@ export const DEFAULT_SETTINGS: Settings = { isPostersTmdbOnly: false, googleAnalyticsId: '', isOutsideAccess: false, + isAnonymized: false, complete: false, }, rewind: { diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 3db3f533..24a3849f 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,7 +1,8 @@ import { Settings } from '@/types/settings' +import { TautulliItemRow } from '@/types/tautulli' import { PERIODS, SETTINGS_PAGES } from './constants' -const requiredSettings = [ +const REQUIRED_SETTINGS = [ 'connection.tautulliUrl', 'connection.tautulliApiKey', 'connection.plexUrl', @@ -14,7 +15,7 @@ const requiredSettings = [ ] export function checkRequiredSettings(settings: Settings): string | null { - for (const key of requiredSettings) { + for (const key of REQUIRED_SETTINGS) { const keys = key.split('.') // @ts-expect-error - TODO: we know this is safe, but should still look to resolve without exception const settingValue = keys.reduce((acc, curr) => acc && acc[curr], settings) @@ -39,3 +40,20 @@ export function getRewindDateRange(settings: Settings) { return { startDate, endDate } } + +export function anonymizeUsers( + users: TautulliItemRow[], + loggedInUserId: string, +): TautulliItemRow[] { + return users.map((user) => { + const isLoggedIn = user.user_id === Number(loggedInUserId) + + return { + ...user, + user: isLoggedIn ? user.user : 'Anonymous', + friendly_name: isLoggedIn ? user.friendly_name : 'Anonymous', + user_thumb: isLoggedIn ? user.user_thumb : '', + user_id: isLoggedIn ? user.user_id : 0, + } + }) +}