From f85ba8638834eaec30db66be0b90ba4276921861 Mon Sep 17 00:00:00 2001 From: Rauno Tegelmann Date: Wed, 24 Jul 2024 12:32:20 +0300 Subject: [PATCH 01/14] docs: update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f85e1164..46c23c22 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ Present [Plex](https://plex.tv) user statistics and habits in a beautiful and or - 📆 Rewind - allows your Plex users view their statistics and habits for a given year. - 👀 Dashboard - provides an easily glanceable overview of activity on your server for all your libraries. - ✨ Beautiful animations with [Framer Motion](https://www.framer.com/motion). -- 🔗 Integrates with [Overseerr](https://overseerr.dev) & [Tautulli](https://tautulli.com). +- 📊 Fuelled by data from [Tautulli](https://tautulli.com) - the backbone responsible for the heavy lifting regarding stats. +- 🔗 Integrates with [Overseerr](https://overseerr.dev) - show request breakdowns and totals. - 🔐 Log in with Plex - uses [NextAuth.js](https://next-auth.js.org) to enable secure login and session management with your Plex account. - 🚀 PWA support - installable on mobile devices and desktops thanks to [Serwist](https://github.com/serwist/serwist). - 🐳 Easy deployment - run the application in a containerized environment with [Docker](https://www.docker.com). From 0755cd8dde5a660080b78bf01ddb8f3f0370e324 Mon Sep 17 00:00:00 2001 From: Rauno Tegelmann Date: Wed, 24 Jul 2024 17:59:00 +0300 Subject: [PATCH 02/14] feat(#171): enable support for ARMv7 devices --- .github/workflows/ci.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61732ae2..94383ea7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: with: context: . push: true - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64,linux/arm64,linux/arm/v7 tags: | ghcr.io/raunot/plex-rewind:${{ github.sha }} build-args: | diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 9c40296b..c6bbc7c9 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -49,7 +49,7 @@ jobs: with: context: . push: true - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64,linux/arm64,linux/arm/v7 tags: | ghcr.io/raunot/plex-rewind:develop ghcr.io/raunot/plex-rewind:${{ env.NEXT_VERSION_TAG }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2581bcfc..8813bba3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: with: context: . push: true - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64,linux/arm64,linux/arm/v7 tags: | ghcr.io/raunot/plex-rewind:latest ghcr.io/raunot/plex-rewind:${{ env.NEXT_VERSION_TAG }} From 0ceeacf1efa79cbd635c3ef36aeb78448ba37f50 Mon Sep 17 00:00:00 2001 From: Rauno Tegelmann Date: Wed, 24 Jul 2024 18:57:42 +0300 Subject: [PATCH 03/14] feat(dashboard): allow toggling total stats --- src/actions/update-feature-settings.ts | 18 +++-- src/app/dashboard/[slug]/page.tsx | 17 +++-- src/app/dashboard/_components/Dashboard.tsx | 25 +++---- src/app/dashboard/users/page.tsx | 68 +++++++++++------ src/app/rewind/_components/RewindStories.tsx | 4 +- src/app/rewind/page.tsx | 4 +- .../_components/ConnectionSettingsForm.tsx | 10 +-- .../_components/FeaturesSettingsForm.tsx | 55 +++++++++++--- src/components/MediaItem/MediaItem.tsx | 4 +- src/components/MediaItem/MediaItems.tsx | 2 +- src/types/index.d.ts | 19 ++++- src/utils/constants.ts | 6 +- src/utils/formatting.ts | 4 +- src/utils/getDashboard.ts | 58 ++++++++------ src/utils/getSettings.ts | 75 ++++++++++++++----- 15 files changed, 252 insertions(+), 117 deletions(-) diff --git a/src/actions/update-feature-settings.ts b/src/actions/update-feature-settings.ts index 94ad13c4..e3ccf7a1 100644 --- a/src/actions/update-feature-settings.ts +++ b/src/actions/update-feature-settings.ts @@ -1,6 +1,10 @@ 'use server' -import { SettingsFormInitialState } from '@/types' +import { + DashboardItemStatistics, + DashboardTotalStatistics, + SettingsFormInitialState, +} from '@/types' import { SETTINGS_PATH } from '@/utils/constants' import getSettings from '@/utils/getSettings' import { promises as fs } from 'fs' @@ -12,7 +16,8 @@ const schema = z.object({ isDashboardActive: z.boolean(), isUsersPageActive: z.boolean(), activeLibraries: z.array(z.string()), - activeDashboardStatistics: z.array(z.string()), + activeDashboardItemStatistics: z.array(z.string()), + activeDashboardTotalStatistics: z.array(z.string()), dashboardDefaultPeriod: z.string().refine( (value) => { const number = parseFloat(value) @@ -35,9 +40,12 @@ export async function saveFeaturesSettings( isDashboardActive: formData.get('isDashboardActive') === 'on', isUsersPageActive: formData.get('isUsersPageActive') === 'on', activeLibraries: formData.getAll('activeLibraries') as string[], - activeDashboardStatistics: formData.getAll( - 'activeDashboardStatistics', - ) as string[], + activeDashboardItemStatistics: formData.getAll( + 'activeDashboardItemStatistics', + ) as DashboardItemStatistics, + activeDashboardTotalStatistics: formData.getAll( + 'activeDashboardTotalStatistics', + ) as DashboardTotalStatistics, dashboardDefaultPeriod: formData.get('dashboardDefaultPeriod') as string, googleAnalyticsId: formData.get('googleAnalyticsId') as string, } diff --git a/src/app/dashboard/[slug]/page.tsx b/src/app/dashboard/[slug]/page.tsx index 399fd68d..d0494191 100644 --- a/src/app/dashboard/[slug]/page.tsx +++ b/src/app/dashboard/[slug]/page.tsx @@ -42,10 +42,17 @@ async function DashboardContent({ params, searchParams }: Props) { const period = getPeriod(searchParams, settings) const [items, totalDuration, totalSize, serverId] = await Promise.all([ getItems(library, period.daysAgo), - getTotalDuration(library, period.string), - getTotalSize(library), + getTotalDuration(library, period.string, settings), + getTotalSize(library, settings), getServerId(), ]) + const isCountActive = + settings.features.activeDashboardTotalStatistics.includes('count') + const countValue = + library.section_type === 'movie' + ? Number(library.count) + : Number(library.child_count) + const count = isCountActive ? countValue.toLocaleString('en-US') : undefined return ( ) diff --git a/src/app/dashboard/_components/Dashboard.tsx b/src/app/dashboard/_components/Dashboard.tsx index 8ebd3685..77957189 100644 --- a/src/app/dashboard/_components/Dashboard.tsx +++ b/src/app/dashboard/_components/Dashboard.tsx @@ -14,9 +14,8 @@ type Props = { title: string items?: TautulliItemRow[] totalDuration?: string - totalSize?: string | null - totalRequests?: number - type?: string + totalSize?: string | number + type: 'movie' | 'show' | 'artist' | 'users' serverId?: string count?: string settings: Settings @@ -27,8 +26,7 @@ export default function Dashboard({ items, totalDuration, totalSize, - totalRequests, - type = '', + type, serverId = '', count, settings, @@ -42,8 +40,9 @@ export default function Dashboard({
    {totalSize && (
  • - + {type === 'users' ? : } {totalSize} + {type === 'users' && ' users'}
  • )} {count && ( @@ -58,7 +57,7 @@ export default function Dashboard({ ? 'episodes' : type === 'artist' ? 'tracks' - : 'users'} + : 'requests'} @@ -69,12 +68,6 @@ export default function Dashboard({ {totalDuration} )} - {!!totalRequests && ( -
  • - - {totalRequests} requests -
  • - )}
{items?.length ? ( @@ -105,7 +98,7 @@ function getTitleIcon(type: string) { return case 'artist': return - default: + case 'users': return } } @@ -118,7 +111,7 @@ function getCountIcon(type: string) { return case 'artist': return - default: - return + case 'users': + return } } diff --git a/src/app/dashboard/users/page.tsx b/src/app/dashboard/users/page.tsx index 85807a86..afa6a564 100644 --- a/src/app/dashboard/users/page.tsx +++ b/src/app/dashboard/users/page.tsx @@ -1,4 +1,4 @@ -import { SearchParams, TautulliItem } from '@/types' +import { SearchParams, Settings, TautulliItem } from '@/types' import { fetchOverseerrUserId, fetchPaginatedOverseerrStats, @@ -47,8 +47,10 @@ async function getUsers( getLibrariesByType('artist'), ]) let usersRequestsCounts: UserRequestCounts[] = [] + const isOverseerrActive = + settings.connection.overseerrUrl && settings.connection.overseerrApiKey - if (settings.connection.overseerrUrl) { + if (isOverseerrActive) { const overseerrUserIds = await Promise.all( users.map(async (user) => { const overseerrId = await fetchOverseerrUserId(String(user.user_id)) @@ -133,30 +135,48 @@ async function getUsers( return users } -async function getTotalDuration(period: string) { - const totalDuration = await fetchTautulli<{ total_duration: string }>( - 'get_history', - { - after: period, - length: 0, - }, - ) +async function getTotalDuration(period: string, settings: Settings) { + if (settings.features.activeDashboardTotalStatistics.includes('duration')) { + const totalDuration = await fetchTautulli<{ total_duration: string }>( + 'get_history', + { + after: period, + length: 0, + }, + ) - return secondsToTime( - timeToSeconds(totalDuration?.response?.data?.total_duration || '0'), - ) + return secondsToTime( + timeToSeconds(totalDuration?.response?.data?.total_duration || '0'), + ) + } + + return undefined } -async function getUsersCount() { - const usersCount = await fetchTautulli<[]>('get_users') +async function getUsersCount(settings: Settings) { + if (settings.features.activeDashboardTotalStatistics.includes('count')) { + const usersCount = await fetchTautulli<[]>('get_users') - return usersCount?.response?.data.slice(1).length || 0 + return usersCount?.response?.data.slice(1).length + } + + return undefined } -async function getTotalRequests(period: string) { - const requests = await fetchPaginatedOverseerrStats('request', period) +async function getTotalRequests(period: string, settings: Settings) { + const isOverseerrActive = + settings.connection.overseerrUrl && settings.connection.overseerrApiKey + + if ( + settings.features.activeDashboardTotalStatistics.includes('requests') && + isOverseerrActive + ) { + const requests = await fetchPaginatedOverseerrStats('request', period) + + return requests.length.toString() + } - return requests.length + return undefined } type Props = { @@ -174,9 +194,9 @@ async function DashboardUsersContent({ searchParams }: Props) { const [usersData, totalDuration, usersCount, totalRequests] = await Promise.all([ getUsers(period.daysAgo, period.date, period.string), - getTotalDuration(period.string), - getUsersCount(), - getTotalRequests(period.date), + getTotalDuration(period.string, settings), + getUsersCount(settings), + getTotalRequests(period.date, settings), ]) return ( @@ -184,10 +204,10 @@ async function DashboardUsersContent({ searchParams }: Props) { title='Users' items={usersData} totalDuration={totalDuration} - count={String(usersCount)} + totalSize={usersCount} type='users' settings={settings} - totalRequests={totalRequests} + count={totalRequests} /> ) } diff --git a/src/app/rewind/_components/RewindStories.tsx b/src/app/rewind/_components/RewindStories.tsx index 12662eef..7023a37d 100644 --- a/src/app/rewind/_components/RewindStories.tsx +++ b/src/app/rewind/_components/RewindStories.tsx @@ -48,13 +48,15 @@ export default function RewindStories({ userRewind, settings }: Props) { } } + const isOverseerrActive = + settings.connection.overseerrUrl && settings.connection.overseerrApiKey const stories = [ createStory(StoryWelcome, 5000), createStory(StoryTotal, 8000), ...(userRewind.libraries_total_size ? [createStory(StoryLibraries, 9000)] : []), - ...(settings.connection.overseerrUrl + ...(isOverseerrActive ? [createStory(StoryRequests, userRewind.requests?.total ? 9000 : 4000)] : []), ...(userRewind.duration.user diff --git a/src/app/rewind/page.tsx b/src/app/rewind/page.tsx index eb7ed23a..3c4a174b 100644 --- a/src/app/rewind/page.tsx +++ b/src/app/rewind/page.tsx @@ -95,8 +95,10 @@ export default async function RewindPage({ searchParams }: Props) { server_id: serverId, user: user, } + const isOverseerrActive = + settings.connection.overseerrUrl && settings.connection.overseerrApiKey - if (settings.connection.overseerrUrl) { + if (isOverseerrActive) { const requestTotals = await getRequestsTotals(user.id) userRewind.requests = requestTotals diff --git a/src/app/settings/connection/_components/ConnectionSettingsForm.tsx b/src/app/settings/connection/_components/ConnectionSettingsForm.tsx index 53a83a6b..727cea43 100644 --- a/src/app/settings/connection/_components/ConnectionSettingsForm.tsx +++ b/src/app/settings/connection/_components/ConnectionSettingsForm.tsx @@ -24,7 +24,7 @@ export default function ConnectionSettingsForm({ settings }: Props) { required defaultValue={connectionSettings.tautulliUrl} /> - Tautulli URL + URL
@@ -47,7 +47,7 @@ export default function ConnectionSettingsForm({ settings }: Props) { defaultValue={connectionSettings.tmdbApiKey} required /> - TMDB API key + API key
@@ -60,7 +60,7 @@ export default function ConnectionSettingsForm({ settings }: Props) { name='overseerrUrl' defaultValue={connectionSettings.overseerrUrl} /> - Overseerr URL + URL
diff --git a/src/app/settings/features/_components/FeaturesSettingsForm.tsx b/src/app/settings/features/_components/FeaturesSettingsForm.tsx index d927d343..e37d3b1b 100644 --- a/src/app/settings/features/_components/FeaturesSettingsForm.tsx +++ b/src/app/settings/features/_components/FeaturesSettingsForm.tsx @@ -13,6 +13,8 @@ type Props = { export default function FeaturesSettingsForm({ settings, libraries }: Props) { const featuresSettings = settings.features + const isOverseerrActive = + settings.connection.overseerrUrl && settings.connection.overseerrApiKey return ( @@ -69,9 +71,9 @@ export default function FeaturesSettingsForm({ settings, libraries }: Props) { + +
+ + + Size + + + + Duration + + + + Count + + {isOverseerrActive && ( + + + Requests + + )}
- +