From ee6aed896c1411f041ace122535937288d1d81ee Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 23 Feb 2025 23:41:05 -0600 Subject: [PATCH] Flag photos without recipes as 'outdated' --- app/admin/outdated/page.tsx | 11 ++---- app/admin/photos/page.tsx | 8 +--- src/admin/AdminOutdatedClient.tsx | 17 +++------ src/admin/AdminPhotoMenuClient.tsx | 11 +++++- src/admin/insights/AdminAppInsights.tsx | 6 +-- src/admin/insights/AdminAppInsightsClient.tsx | 6 ++- src/admin/insights/actions.ts | 7 ++-- src/photo/db/index.ts | 9 ++++- src/photo/db/query.ts | 38 +++++++++++++++++++ src/photo/index.ts | 2 - src/photo/outdated.ts | 13 +++++++ 11 files changed, 89 insertions(+), 39 deletions(-) create mode 100644 src/photo/outdated.ts diff --git a/app/admin/outdated/page.tsx b/app/admin/outdated/page.tsx index ee30bc28..4f012dca 100644 --- a/app/admin/outdated/page.tsx +++ b/app/admin/outdated/page.tsx @@ -1,17 +1,12 @@ -import { getPhotos } from '@/photo/db/query'; -import { OUTDATED_THRESHOLD } from '@/photo'; import AdminOutdatedClient from '@/admin/AdminOutdatedClient'; import { AI_TEXT_GENERATION_ENABLED } from '@/app/config'; +import { getOutdatedPhotos } from '@/photo/db/query'; export const maxDuration = 60; export default async function AdminOutdatedPage() { - const photos = await getPhotos({ - hidden: 'include', - sortBy: 'createdAtAsc', - updatedBefore: OUTDATED_THRESHOLD, - limit: 1_000, - }).catch(() => []); + const photos = await getOutdatedPhotos() + .catch(() => []); return ( count) .catch(() => 0), - getPhotosMetaCached({ - hidden: 'include', - updatedBefore: OUTDATED_THRESHOLD, - }) - .then(({ count }) => count) + getOutdatedPhotosCount() .catch(() => 0), DEBUG_PHOTO_BLOBS ? getStoragePhotoUrlsNoStore() diff --git a/src/admin/AdminOutdatedClient.tsx b/src/admin/AdminOutdatedClient.tsx index ad3f7f6b..c108c0ef 100644 --- a/src/admin/AdminOutdatedClient.tsx +++ b/src/admin/AdminOutdatedClient.tsx @@ -1,6 +1,6 @@ 'use client'; -import { OUTDATED_THRESHOLD, Photo } from '@/photo'; +import { Photo } from '@/photo'; import AdminPhotosTable from '@/admin/AdminPhotosTable'; import LoaderButton from '@/components/primitives/LoaderButton'; import IconGrSync from '@/app/IconGrSync'; @@ -80,18 +80,13 @@ export default function AdminOutdatedClient({
- Outdated photos found + {photos.length} outdated + {' '} + {photos.length === 1 ? 'photo' : 'photos'} found
- {photos.length} + They may have missing EXIF fields, inaccurate blur data, {' '} - {photos.length === 1 ? 'photo' : 'photos'} - {' ('}last updated before - {' '} - {new Date(OUTDATED_THRESHOLD).toLocaleDateString()}{')'} - {' '} - may have: missing EXIF fields, inaccurate blur data, - {' '} - undesired privacy settings, or missing AI-generated text + undesired privacy settings, or text that can be AI-generated
diff --git a/src/admin/AdminPhotoMenuClient.tsx b/src/admin/AdminPhotoMenuClient.tsx index c9fc77b8..3209b68c 100644 --- a/src/admin/AdminPhotoMenuClient.tsx +++ b/src/admin/AdminPhotoMenuClient.tsx @@ -22,6 +22,8 @@ import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll'; import { MdOutlineFileDownload } from 'react-icons/md'; import MoreMenuItem from '@/components/more/MoreMenuItem'; import IconGrSync from '@/app/IconGrSync'; +import { isPhotoOutdated } from '@/photo/outdated'; +import { FaCircle } from 'react-icons/fa6'; export default function AdminPhotoMenuClient({ photo, @@ -76,7 +78,14 @@ export default function AdminPhotoMenuClient({ hrefDownloadName: downloadFileNameForPhoto(photo), }); items.push({ - label: 'Sync', + label: + Sync + {isPhotoOutdated(photo) && + } + , icon: , action: () => syncPhotoAction(photo.id) .then(() => revalidatePhoto?.(photo.id)), diff --git a/src/admin/insights/AdminAppInsights.tsx b/src/admin/insights/AdminAppInsights.tsx index 4f5ab18f..d6f387eb 100644 --- a/src/admin/insights/AdminAppInsights.tsx +++ b/src/admin/insights/AdminAppInsights.tsx @@ -12,8 +12,8 @@ import { HAS_STATIC_OPTIMIZATION, MATTE_PHOTOS, } from '@/app/config'; -import { OUTDATED_THRESHOLD } from '@/photo'; import { getGitHubMetaForCurrentApp, getSignificantInsights } from '.'; +import { getOutdatedPhotosCount } from '@/photo/db/query'; const BASIC_PHOTO_INSTALLATION_COUNT = 32; @@ -21,7 +21,7 @@ export default async function AdminAppInsights() { const [ { count: photosCount, dateRange }, { count: photosCountHidden }, - { count: photosCountOutdated }, + photosCountOutdated, { count: photosCountPortrait }, tags, cameras, @@ -31,7 +31,7 @@ export default async function AdminAppInsights() { ] = await Promise.all([ getPhotosMeta({ hidden: 'include' }), getPhotosMeta({ hidden: 'only' }), - getPhotosMeta({ hidden: 'include', updatedBefore: OUTDATED_THRESHOLD }), + getOutdatedPhotosCount(), getPhotosMeta({ maximumAspectRatio: 0.9 }), getUniqueTags(), getUniqueCameras(), diff --git a/src/admin/insights/AdminAppInsightsClient.tsx b/src/admin/insights/AdminAppInsightsClient.tsx index c1fe6ccb..f735fce1 100644 --- a/src/admin/insights/AdminAppInsightsClient.tsx +++ b/src/admin/insights/AdminAppInsightsClient.tsx @@ -342,8 +342,10 @@ export default function AdminAppInsightsClient({ )} />} content={renderHighlightText( - // eslint-disable-next-line max-len - pluralize(photosCountOutdated || DEBUG_PHOTOS_COUNT_OUTDATED, 'outdated photo'), + pluralize( + photosCountOutdated || DEBUG_PHOTOS_COUNT_OUTDATED, + 'outdated photo', + ), 'yellow', )} expandPath={PATH_ADMIN_OUTDATED} diff --git a/src/admin/insights/actions.ts b/src/admin/insights/actions.ts index 6bb2bd2c..ed6c03f9 100644 --- a/src/admin/insights/actions.ts +++ b/src/admin/insights/actions.ts @@ -6,18 +6,17 @@ import { getSignificantInsights, InsightIndicatorStatus, } from '.'; -import { getPhotosMeta } from '@/photo/db/query'; -import { OUTDATED_THRESHOLD } from '@/photo'; +import { getOutdatedPhotosCount } from '@/photo/db/query'; export const getShouldShowInsightsIndicatorAction = async (): Promise => runAuthenticatedAdminServerAction(async () => { const [ codeMeta, - { count: photosCountOutdated }, + photosCountOutdated, ] = await Promise.all([ getGitHubMetaForCurrentApp(), - getPhotosMeta({ hidden: 'include', updatedBefore: OUTDATED_THRESHOLD }), + getOutdatedPhotosCount(), ]); const { diff --git a/src/photo/db/index.ts b/src/photo/db/index.ts index 544e29b6..ac1e7149 100644 --- a/src/photo/db/index.ts +++ b/src/photo/db/index.ts @@ -1,6 +1,7 @@ import { PRIORITY_ORDER_ENABLED } from '@/app/config'; import { parameterize } from '@/utility/string'; import { PhotoSetCategory } from '..'; +import { Camera } from '@/camera'; export const GENERATE_STATIC_PARAMS_LIMIT = 1000; export const PHOTO_DEFAULT_LIMIT = 100; @@ -22,7 +23,9 @@ export type GetPhotosOptions = { takenAfterInclusive?: Date updatedBefore?: Date hidden?: 'exclude' | 'include' | 'only' -} & PhotoSetCategory; +} & Omit & { + camera?: Partial +}; export const areOptionsSensitive = (options: GetPhotosOptions) => options.hidden === 'include' || options.hidden === 'only'; @@ -83,9 +86,11 @@ export const getWheresFromOptions = ( wheres.push(`$${valuesIndex++}=ANY(tags)`); wheresValues.push(tag); } - if (camera) { + if (camera?.make) { wheres.push(`${parameterizeForDb('make')}=$${valuesIndex++}`); wheresValues.push(parameterize(camera.make, true)); + } + if (camera?.model) { wheres.push(`${parameterizeForDb('model')}=$${valuesIndex++}`); wheresValues.push(parameterize(camera.model, true)); } diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index d339196b..3bdc1709 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -24,6 +24,8 @@ import { getWheresFromOptions } from '.'; import { FocalLengths } from '@/focal'; import { Lenses, createLensKey } from '@/lens'; import { migrationForError } from './migration'; +import { UPDATED_BEFORE_01, UPDATED_BEFORE_02 } from '../outdated'; +import { MAKE_FUJIFILM } from '@/platforms/fujifilm'; const createPhotosTable = () => sql` @@ -445,3 +447,39 @@ export const getPhoto = async ( .then(({ rows }) => rows.map(parsePhotoFromDb)) .then(photos => photos.length > 0 ? photos[0] : undefined); }, 'getPhoto'); + +// Outdated queries + +const outdatedWhereClause = + // eslint-disable-next-line quotes + `WHERE updated_at < $1 OR (updated_at < $2 AND make = $3)`; + +const outdatedValues = [ + UPDATED_BEFORE_01.toISOString(), + UPDATED_BEFORE_02.toISOString(), + MAKE_FUJIFILM, +]; + +export const getOutdatedPhotos = () => safelyQueryPhotos( + () => query(` + SELECT * FROM photos + ${outdatedWhereClause} + ORDER BY created_at ASC + LIMIT 1000 + `, + outdatedValues, + ) + .then(({ rows }) => rows.map(parsePhotoFromDb)), + 'getOutdatedPhotos', +); + +export const getOutdatedPhotosCount = () => safelyQueryPhotos( + () => query(` + SELECT COUNT(*) FROM photos + ${outdatedWhereClause} + `, + outdatedValues, + ) + .then(({ rows }) => parseInt(rows[0].count, 10)), + 'getOutdatedPhotosCount', +); diff --git a/src/photo/index.ts b/src/photo/index.ts index 57c3f93e..b6f93c5a 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -22,8 +22,6 @@ import { isBefore } from 'date-fns'; import type { Metadata } from 'next'; import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; -export const OUTDATED_THRESHOLD = new Date('2024-06-16'); - // INFINITE SCROLL: FEED export const INFINITE_SCROLL_FEED_INITIAL = process.env.NODE_ENV === 'development' ? 2 : 12; diff --git a/src/photo/outdated.ts b/src/photo/outdated.ts new file mode 100644 index 00000000..31d1fdfe --- /dev/null +++ b/src/photo/outdated.ts @@ -0,0 +1,13 @@ +import { MAKE_FUJIFILM } from '@/platforms/fujifilm'; +import { Photo } from '.'; + +export const UPDATED_BEFORE_01 = new Date('2024-06-16'); +// UTC 2025-02-24 05:30:00 +export const UPDATED_BEFORE_02 = new Date(Date.UTC(2025, 1, 24, 5, 30, 0)); + +export const isPhotoOutdated = (photo: Photo) => { + return photo.updatedAt < UPDATED_BEFORE_01 || ( + photo.updatedAt < UPDATED_BEFORE_02 && + photo.make === MAKE_FUJIFILM + ); +};