From 710c297fc206a002719e686353a35c62b6f90121 Mon Sep 17 00:00:00 2001 From: Christian Benincasa Date: Mon, 29 Apr 2024 15:37:51 -0400 Subject: [PATCH] Upsert programs before calculating groupings (#401) * Upsert programs before calculating groupings This seems to resolve an issue where in the grouping upsert code, the entity manager attempts to insert programs that it finds in its entity map (it sees the objects as created and ready to be persisted). However, we need special upsert code for the programs due to various unique key constraints. This simply reoders the operations to save the programs first and then calculate their groupings. Also fixes a weird state bug I was experiencing in the program selector * Fixes for a bunch of different InlineModal bugs 1. Fixes nested inline modals that had 'last row' functionality of wrapping (i.e. intermediate num items < row length) 2. Fixes last item index calculation function -- I slightly misunderstood what this was doing when rewriting it. 3. Fixes near-constant re-rendering of all InlineModals and PlexGridItem children due to a bad effect setup --- server/src/dao/programHelpers.ts | 41 +++++++------- web/src/components/InlineModal.tsx | 56 +++++++++---------- .../channel_config/PlexGridItem.tsx | 1 + .../PlexProgrammingSelector.tsx | 28 ++++++---- web/src/helpers/inlineModalUtil.ts | 7 ++- web/src/store/programmingSelector/actions.ts | 20 ++++--- 6 files changed, 79 insertions(+), 74 deletions(-) diff --git a/server/src/dao/programHelpers.ts b/server/src/dao/programHelpers.ts index df497e7d0..49e1f2d42 100644 --- a/server/src/dao/programHelpers.ts +++ b/server/src/dao/programHelpers.ts @@ -46,7 +46,7 @@ import { } from '../util/index.js'; import { ProgramMinterFactory } from '../util/programMinter.js'; import { ProgramSourceType } from './custom_types/ProgramSourceType.js'; -import { getEm, withDb } from './dataSource.js'; +import { getEm } from './dataSource.js'; import { PlexServerSettings } from './entities/PlexServerSettings.js'; import { Program, ProgramType } from './entities/Program.js'; import { @@ -123,20 +123,25 @@ export async function upsertContentPrograms( .mapValues((programs) => groupBy(programs, (p) => p.externalSourceName!)) .value() as ProgramsBySource; + logger.debug('Upserting %d programs', programsToPersist.length); + + const upsertedPrograms = flatten( + await mapAsyncSeq(chunk(programsToPersist, batchSize), (programs) => + em.upsertMany(Program, programs, { + onConflictAction: 'merge', + onConflictFields: ['sourceType', 'externalSourceId', 'externalKey'], + onConflictExcludeFields: ['uuid'], + }), + ), + ); + // Fork a new entity manager here so we don't attempt to persist anything // in the parent context. This function potentially does a lot of work // but we don't want to accidentally not do an upsert of a program. - const programGroupingsBySource = await withDb( - async () => { - return await findAndUpdateProgramRelations(programsBySource); - }, - undefined, - true, - ); + const programGroupingsBySource = + await findAndUpdateProgramRelations(programsBySource); - logger.debug('Upserting %d programs', programsToPersist.length); - - forEach(programsToPersist, (program) => { + forEach(upsertedPrograms, (program) => { if ( program.type !== ProgramType.Episode && program.type !== ProgramType.Track @@ -215,15 +220,7 @@ export async function upsertContentPrograms( } }); - const upsertedPrograms = flatten( - await mapAsyncSeq(chunk(programsToPersist, batchSize), (programs) => - em.upsertMany(Program, programs, { - onConflictAction: 'merge', - onConflictFields: ['sourceType', 'externalSourceId', 'externalKey'], - onConflictExcludeFields: ['uuid'], - }), - ), - ); + await em.flush(); return upsertedPrograms; } @@ -289,9 +286,9 @@ async function findAndUpdatePlexServerPrograms( return; } - const em = getEm(); + const em = getEm().fork(); - const plexServer = await getEm().findOne(PlexServerSettings, { + const plexServer = await em.findOne(PlexServerSettings, { name: plexServerName, }); diff --git a/web/src/components/InlineModal.tsx b/web/src/components/InlineModal.tsx index bcdd5eec3..3e162a34f 100644 --- a/web/src/components/InlineModal.tsx +++ b/web/src/components/InlineModal.tsx @@ -1,13 +1,7 @@ import { Box, Collapse, List } from '@mui/material'; -import { PlexMedia, isPlexMedia, isTerminalItem } from '@tunarr/types/plex'; +import { PlexMedia, isPlexMedia, isPlexParentItem } from '@tunarr/types/plex'; import { usePrevious } from '@uidotdev/usehooks'; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { useEffect, useCallback, useRef, useState, useMemo } from 'react'; import { extractLastIndexes, findFirstItemInNextRowIndex, @@ -17,8 +11,9 @@ import { import _ from 'lodash-es'; import useStore from '../store'; import { PlexGridItem } from './channel_config/PlexGridItem'; -import { useIntersectionObserver } from 'usehooks-ts'; import { toggle } from '../helpers/util.ts'; +import React from 'react'; +import { useIntersectionObserver } from 'usehooks-ts'; type InlineModalProps = { itemGuid: string; @@ -76,7 +71,6 @@ export function InlineModal(props: InlineModalProps) { const containerWidth = ref?.current?.getBoundingClientRect().width || 0; const itemWidth = gridItemRef?.current?.getBoundingClientRect().width || 0; - const imagesPerRow = getImagesPerRow(containerWidth, itemWidth); setChildLimit(imagesPerRow * 2); setItemWidth(itemWidth); @@ -85,24 +79,29 @@ export function InlineModal(props: InlineModalProps) { } }, [ref, gridItemRef, previousItemGuid, itemGuid]); + const [childItemGuid, setChildItemGuid] = useState(null); const [childModalIndex, setChildModalIndex] = useState(-1); - const handleMoveModal = useCallback( - (index: number) => { - if (index === childModalIndex) { - setChildModalIndex(-1); - } else { - setChildModalIndex(index); - } - }, - [childModalIndex], + const firstItemInNextRowIndex = useMemo( + () => + findFirstItemInNextRowIndex( + childModalIndex, + rowSize, + modalChildren?.length ?? 0, + ), + [childModalIndex, modalChildren?.length, rowSize], ); + const handleMoveModal = useCallback((index: number, item: PlexMedia) => { + setChildItemGuid((prev) => (prev === item.guid ? null : item.guid)); + setChildModalIndex((prev) => (prev === index ? -1 : index)); + }, []); + // TODO: Complete this by updating the limit below, not doing this // right now because already working with a huge changeset. const { ref: intersectionRef } = useIntersectionObserver({ onChange: (_, entry) => { - if (entry.isIntersecting) { + if (entry.isIntersecting && isOpen) { if (childLimit < modalChildren.length) { setChildLimit((prev) => prev + imagesPerRow * 2); } @@ -118,6 +117,8 @@ export function InlineModal(props: InlineModalProps) { ).includes(childModalIndex) : false; + console.log('my children, should be open', type, modalChildren); + return ( ( - {!isTerminalItem(child) && ( + {isPlexParentItem(child) && ( @@ -183,7 +177,7 @@ export function InlineModal(props: InlineModalProps) { @@ -192,7 +186,7 @@ export function InlineModal(props: InlineModalProps) { .value()} {/* This Modal is for last row items because they can't be inserted using the above inline modal */} { if (!isUndefined(children?.Metadata)) { + console.log(item.guid, children.Metadata); addKnownMediaForServer(server.name, children.Metadata, item.guid); } }, [item.guid, server.name, children]); diff --git a/web/src/components/channel_config/PlexProgrammingSelector.tsx b/web/src/components/channel_config/PlexProgrammingSelector.tsx index 9a7e0946f..5f6956751 100644 --- a/web/src/components/channel_config/PlexProgrammingSelector.tsx +++ b/web/src/components/channel_config/PlexProgrammingSelector.tsx @@ -138,7 +138,9 @@ export default function PlexProgrammingSelector() { const imageWidth = imageRef?.getBoundingClientRect().width; // 16 is additional padding available in the parent container - setRowSize(getImagesPerRow(width ? width + 16 : 0, imageWidth || 0)); + const rowSize = getImagesPerRow(width ? width + 16 : 0, imageWidth || 0); + setRowSize(rowSize); + setScrollParams(({ max }) => ({ max, limit: rowSize * 4 })); } }, [width, tabValue, viewType, modalGuid]); @@ -319,19 +321,24 @@ export default function PlexProgrammingSelector() { }); useEffect(() => { - if (!isUndefined(searchData)) { - // We're using this as an analogue for detecting the start of a new 'query' - if (searchData.pages.length === 1) { - const max = searchData.pages[0].totalSize ?? searchData.pages[0].size; - setScrollParams({ - limit: rowSize * 4, - max, - }); + if (searchData?.pages.length === 1) { + const size = searchData.pages[0].totalSize ?? searchData.pages[0].size; + if (scrollParams.max !== size) { + console.log('herehereh'); + setScrollParams(({ limit }) => ({ + limit, + max: size, + })); } + } + }, [searchData?.pages, scrollParams.max]); + useEffect(() => { + if (!isUndefined(searchData)) { // We probably wouldn't have made it this far if we didnt have a server, but // putting this here to prevent crashes if (selectedServer) { + console.log('still in this one'); const allMedia = chain(searchData.pages) .reject((page) => page.size === 0) .map((page) => page.Metadata) @@ -340,7 +347,7 @@ export default function PlexProgrammingSelector() { addKnownMediaForServer(selectedServer.name, allMedia); } } - }, [selectedServer, searchData, setScrollParams, rowSize]); + }, [scrollParams, selectedServer, searchData, rowSize]); useEffect(() => { if ( @@ -407,7 +414,6 @@ export default function PlexProgrammingSelector() { (tabValue === 0 ? firstItemInNexLibraryRowIndex : firstItemInNextCollectionRowIndex); - return ( {isPlexParentItem(item) && ( diff --git a/web/src/helpers/inlineModalUtil.ts b/web/src/helpers/inlineModalUtil.ts index 9ba39db2c..1dae37189 100644 --- a/web/src/helpers/inlineModalUtil.ts +++ b/web/src/helpers/inlineModalUtil.ts @@ -112,5 +112,10 @@ export function findFirstItemInNextRowIndex( } export function extractLastIndexes(arr: PlexMedia[], x: number): number[] { - return range(x > arr.length ? 0 : x, arr.length); + const indexes = range(0, arr.length); + if (x > arr.length) { + return indexes; + } + + return indexes.slice(-x); } diff --git a/web/src/store/programmingSelector/actions.ts b/web/src/store/programmingSelector/actions.ts index d70a663f1..03da53b8a 100644 --- a/web/src/store/programmingSelector/actions.ts +++ b/web/src/store/programmingSelector/actions.ts @@ -4,7 +4,6 @@ import { PlexLibrarySection, PlexMedia, isPlexDirectory, - isTerminalItem, } from '@tunarr/types/plex'; import { map, reject, some, uniq } from 'lodash-es'; import useStore from '..'; @@ -68,20 +67,23 @@ export const addKnownMediaForServer = ( hierarchy = state.contentHierarchyByServer[serverName]; } - const childrenByGuid = plexMedia - .filter((m) => !isTerminalItem(m)) - .reduce((prev, media) => ({ ...prev, [uniqueId(media)]: [] }), {}); - - state.contentHierarchyByServer[serverName] = { - ...state.contentHierarchyByServer[serverName], - ...childrenByGuid, - }; + // plexMedia + // .filter((m) => !isTerminalItem(m)) + // .map(uniqueId) + // .forEach((id) => { + // if (!has(state.contentHierarchyByServer[serverName], id)) { + // state.contentHierarchyByServer[serverName][id] = []; + // } + // }); if (parentId) { + console.log('setting parent id', parentId); if (!state.contentHierarchyByServer[serverName][parentId]) { state.contentHierarchyByServer[serverName][parentId] = []; } + console.log(plexMedia.map(uniqueId)); + state.contentHierarchyByServer[serverName][parentId] = uniq([ ...state.contentHierarchyByServer[serverName][parentId], ...plexMedia.map(uniqueId),