Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): add option to anonymize other users #263

Merged
merged 4 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/app/dashboard/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}

Expand Down
2 changes: 2 additions & 0 deletions src/app/settings/general/_actions/updateGeneralSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const schema = z.object({
isPostersTmdbOnly: z.boolean(),
googleAnalyticsId: z.string(),
isOutsideAccess: z.boolean(),
isAnonymized: z.boolean(),
complete: z.boolean(),
})

Expand All @@ -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,
}

Expand Down
11 changes: 11 additions & 0 deletions src/app/settings/general/_components/GeneralSettingsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ export default function GeneralSettingsForm({ settings, libraries }: Props) {
<small>Access without login.</small>
</span>
</Switch>
<Switch
className='switch items-start'
name='isAnonymized'
defaultSelected={generalSettings.isAnonymized}
>
<div className='indicator'></div>
<span className='label'>
<span className='label-wrapper'>Anonymize</span>
<small>Hide usernames for other users.</small>
</span>
</Switch>
</section>
<section className='group-settings group'>
<h2 className='heading-settings'>Analytics</h2>
Expand Down
18 changes: 13 additions & 5 deletions src/components/MediaItem/MediaItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string>(initialImageSrc)
const [dataKey, setDataKey] = useState<number>(0)
const titleContainerRef = useRef<HTMLDivElement>(null)
const isOverseerrActive =
settings.connection.overseerrUrl && settings.connection.overseerrApiKey
const [imageSrc, setImageSrc] = useState<string>(posterSrc)

useEffect(() => {
setDataKey((prevDataKey) => prevDataKey + 1)
setImageSrc(posterSrc)
}, [data, type, posterSrc])
setImageSrc(initialImageSrc)
}, [data, type, initialImageSrc])

return (
<motion.li
Expand All @@ -89,7 +93,11 @@ export default function MediaItem({
<Image
fill
className='object-cover object-top'
alt={isUserDashboard ? data.user + ' avatar' : data.title + ' poster'}
alt={
isUserDashboard
? data.friendly_name + ' avatar'
: data.title + ' poster'
}
src={imageSrc}
sizes='10rem'
onError={() => setImageSrc(placeholderSvg)}
Expand Down
8 changes: 8 additions & 0 deletions src/components/MediaItem/anonymous.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/types/settings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type GeneralSettings = {
isPostersTmdbOnly: boolean
googleAnalyticsId: string
isOutsideAccess: boolean
isAnonymized: boolean
complete: boolean
}

Expand Down
1 change: 1 addition & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const DEFAULT_SETTINGS: Settings = {
isPostersTmdbOnly: false,
googleAnalyticsId: '',
isOutsideAccess: false,
isAnonymized: false,
complete: false,
},
rewind: {
Expand Down
22 changes: 20 additions & 2 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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)
Expand All @@ -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,
}
})
}