Flag photos without recipes as 'outdated'

This commit is contained in:
Sam Becker 2025-02-23 23:41:05 -06:00
parent 34667efedf
commit ee6aed896c
11 changed files with 89 additions and 39 deletions

View File

@ -1,17 +1,12 @@
import { getPhotos } from '@/photo/db/query';
import { OUTDATED_THRESHOLD } from '@/photo';
import AdminOutdatedClient from '@/admin/AdminOutdatedClient'; import AdminOutdatedClient from '@/admin/AdminOutdatedClient';
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config'; import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
import { getOutdatedPhotos } from '@/photo/db/query';
export const maxDuration = 60; export const maxDuration = 60;
export default async function AdminOutdatedPage() { export default async function AdminOutdatedPage() {
const photos = await getPhotos({ const photos = await getOutdatedPhotos()
hidden: 'include', .catch(() => []);
sortBy: 'createdAtAsc',
updatedBefore: OUTDATED_THRESHOLD,
limit: 1_000,
}).catch(() => []);
return ( return (
<AdminOutdatedClient {...{ <AdminOutdatedClient {...{

View File

@ -1,11 +1,11 @@
import { getStoragePhotoUrlsNoStore } from '@/platforms/storage/cache'; import { getStoragePhotoUrlsNoStore } from '@/platforms/storage/cache';
import { getPhotos } from '@/photo/db/query'; import { getPhotos } from '@/photo/db/query';
import { getPhotosMetaCached } from '@/photo/cache'; import { getPhotosMetaCached } from '@/photo/cache';
import { OUTDATED_THRESHOLD } from '@/photo';
import AdminPhotosClient from '@/admin/AdminPhotosClient'; import AdminPhotosClient from '@/admin/AdminPhotosClient';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { TIMEZONE_COOKIE_NAME } from '@/utility/timezone'; import { TIMEZONE_COOKIE_NAME } from '@/utility/timezone';
import { getOutdatedPhotosCount } from '@/photo/db/query';
export const maxDuration = 60; export const maxDuration = 60;
@ -31,11 +31,7 @@ export default async function AdminPhotosPage() {
getPhotosMetaCached({ hidden: 'include'}) getPhotosMetaCached({ hidden: 'include'})
.then(({ count }) => count) .then(({ count }) => count)
.catch(() => 0), .catch(() => 0),
getPhotosMetaCached({ getOutdatedPhotosCount()
hidden: 'include',
updatedBefore: OUTDATED_THRESHOLD,
})
.then(({ count }) => count)
.catch(() => 0), .catch(() => 0),
DEBUG_PHOTO_BLOBS DEBUG_PHOTO_BLOBS
? getStoragePhotoUrlsNoStore() ? getStoragePhotoUrlsNoStore()

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { OUTDATED_THRESHOLD, Photo } from '@/photo'; import { Photo } from '@/photo';
import AdminPhotosTable from '@/admin/AdminPhotosTable'; import AdminPhotosTable from '@/admin/AdminPhotosTable';
import LoaderButton from '@/components/primitives/LoaderButton'; import LoaderButton from '@/components/primitives/LoaderButton';
import IconGrSync from '@/app/IconGrSync'; import IconGrSync from '@/app/IconGrSync';
@ -80,18 +80,13 @@ export default function AdminOutdatedClient({
<Note> <Note>
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="font-bold"> <div className="font-bold">
Outdated photos found {photos.length} outdated
{' '}
{photos.length === 1 ? 'photo' : 'photos'} found
</div> </div>
{photos.length} They may have missing EXIF fields, inaccurate blur data,
{' '} {' '}
{photos.length === 1 ? 'photo' : 'photos'} undesired privacy settings, or text that can be AI-generated
{' ('}last updated before
{' '}
{new Date(OUTDATED_THRESHOLD).toLocaleDateString()}{')'}
{' '}
may have: missing EXIF fields, inaccurate blur data,
{' '}
undesired privacy settings, or missing AI-generated text
</div> </div>
</Note> </Note>
<div className="space-y-4"> <div className="space-y-4">

View File

@ -22,6 +22,8 @@ import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
import { MdOutlineFileDownload } from 'react-icons/md'; import { MdOutlineFileDownload } from 'react-icons/md';
import MoreMenuItem from '@/components/more/MoreMenuItem'; import MoreMenuItem from '@/components/more/MoreMenuItem';
import IconGrSync from '@/app/IconGrSync'; import IconGrSync from '@/app/IconGrSync';
import { isPhotoOutdated } from '@/photo/outdated';
import { FaCircle } from 'react-icons/fa6';
export default function AdminPhotoMenuClient({ export default function AdminPhotoMenuClient({
photo, photo,
@ -76,7 +78,14 @@ export default function AdminPhotoMenuClient({
hrefDownloadName: downloadFileNameForPhoto(photo), hrefDownloadName: downloadFileNameForPhoto(photo),
}); });
items.push({ items.push({
label: 'Sync', label: <span className="inline-flex items-center gap-2">
<span>Sync</span>
{isPhotoOutdated(photo) &&
<FaCircle
size={8}
className="text-amber-500 translate-y-[1.5px]"
/>}
</span>,
icon: <IconGrSync className="translate-x-[-1px]" />, icon: <IconGrSync className="translate-x-[-1px]" />,
action: () => syncPhotoAction(photo.id) action: () => syncPhotoAction(photo.id)
.then(() => revalidatePhoto?.(photo.id)), .then(() => revalidatePhoto?.(photo.id)),

View File

@ -12,8 +12,8 @@ import {
HAS_STATIC_OPTIMIZATION, HAS_STATIC_OPTIMIZATION,
MATTE_PHOTOS, MATTE_PHOTOS,
} from '@/app/config'; } from '@/app/config';
import { OUTDATED_THRESHOLD } from '@/photo';
import { getGitHubMetaForCurrentApp, getSignificantInsights } from '.'; import { getGitHubMetaForCurrentApp, getSignificantInsights } from '.';
import { getOutdatedPhotosCount } from '@/photo/db/query';
const BASIC_PHOTO_INSTALLATION_COUNT = 32; const BASIC_PHOTO_INSTALLATION_COUNT = 32;
@ -21,7 +21,7 @@ export default async function AdminAppInsights() {
const [ const [
{ count: photosCount, dateRange }, { count: photosCount, dateRange },
{ count: photosCountHidden }, { count: photosCountHidden },
{ count: photosCountOutdated }, photosCountOutdated,
{ count: photosCountPortrait }, { count: photosCountPortrait },
tags, tags,
cameras, cameras,
@ -31,7 +31,7 @@ export default async function AdminAppInsights() {
] = await Promise.all([ ] = await Promise.all([
getPhotosMeta({ hidden: 'include' }), getPhotosMeta({ hidden: 'include' }),
getPhotosMeta({ hidden: 'only' }), getPhotosMeta({ hidden: 'only' }),
getPhotosMeta({ hidden: 'include', updatedBefore: OUTDATED_THRESHOLD }), getOutdatedPhotosCount(),
getPhotosMeta({ maximumAspectRatio: 0.9 }), getPhotosMeta({ maximumAspectRatio: 0.9 }),
getUniqueTags(), getUniqueTags(),
getUniqueCameras(), getUniqueCameras(),

View File

@ -342,8 +342,10 @@ export default function AdminAppInsightsClient({
)} )}
/>} />}
content={renderHighlightText( content={renderHighlightText(
// eslint-disable-next-line max-len pluralize(
pluralize(photosCountOutdated || DEBUG_PHOTOS_COUNT_OUTDATED, 'outdated photo'), photosCountOutdated || DEBUG_PHOTOS_COUNT_OUTDATED,
'outdated photo',
),
'yellow', 'yellow',
)} )}
expandPath={PATH_ADMIN_OUTDATED} expandPath={PATH_ADMIN_OUTDATED}

View File

@ -6,18 +6,17 @@ import {
getSignificantInsights, getSignificantInsights,
InsightIndicatorStatus, InsightIndicatorStatus,
} from '.'; } from '.';
import { getPhotosMeta } from '@/photo/db/query'; import { getOutdatedPhotosCount } from '@/photo/db/query';
import { OUTDATED_THRESHOLD } from '@/photo';
export const getShouldShowInsightsIndicatorAction = export const getShouldShowInsightsIndicatorAction =
async (): Promise<InsightIndicatorStatus> => async (): Promise<InsightIndicatorStatus> =>
runAuthenticatedAdminServerAction(async () => { runAuthenticatedAdminServerAction(async () => {
const [ const [
codeMeta, codeMeta,
{ count: photosCountOutdated }, photosCountOutdated,
] = await Promise.all([ ] = await Promise.all([
getGitHubMetaForCurrentApp(), getGitHubMetaForCurrentApp(),
getPhotosMeta({ hidden: 'include', updatedBefore: OUTDATED_THRESHOLD }), getOutdatedPhotosCount(),
]); ]);
const { const {

View File

@ -1,6 +1,7 @@
import { PRIORITY_ORDER_ENABLED } from '@/app/config'; import { PRIORITY_ORDER_ENABLED } from '@/app/config';
import { parameterize } from '@/utility/string'; import { parameterize } from '@/utility/string';
import { PhotoSetCategory } from '..'; import { PhotoSetCategory } from '..';
import { Camera } from '@/camera';
export const GENERATE_STATIC_PARAMS_LIMIT = 1000; export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
export const PHOTO_DEFAULT_LIMIT = 100; export const PHOTO_DEFAULT_LIMIT = 100;
@ -22,7 +23,9 @@ export type GetPhotosOptions = {
takenAfterInclusive?: Date takenAfterInclusive?: Date
updatedBefore?: Date updatedBefore?: Date
hidden?: 'exclude' | 'include' | 'only' hidden?: 'exclude' | 'include' | 'only'
} & PhotoSetCategory; } & Omit<PhotoSetCategory, 'camera'> & {
camera?: Partial<Camera>
};
export const areOptionsSensitive = (options: GetPhotosOptions) => export const areOptionsSensitive = (options: GetPhotosOptions) =>
options.hidden === 'include' || options.hidden === 'only'; options.hidden === 'include' || options.hidden === 'only';
@ -83,9 +86,11 @@ export const getWheresFromOptions = (
wheres.push(`$${valuesIndex++}=ANY(tags)`); wheres.push(`$${valuesIndex++}=ANY(tags)`);
wheresValues.push(tag); wheresValues.push(tag);
} }
if (camera) { if (camera?.make) {
wheres.push(`${parameterizeForDb('make')}=$${valuesIndex++}`); wheres.push(`${parameterizeForDb('make')}=$${valuesIndex++}`);
wheresValues.push(parameterize(camera.make, true)); wheresValues.push(parameterize(camera.make, true));
}
if (camera?.model) {
wheres.push(`${parameterizeForDb('model')}=$${valuesIndex++}`); wheres.push(`${parameterizeForDb('model')}=$${valuesIndex++}`);
wheresValues.push(parameterize(camera.model, true)); wheresValues.push(parameterize(camera.model, true));
} }

View File

@ -24,6 +24,8 @@ import { getWheresFromOptions } from '.';
import { FocalLengths } from '@/focal'; import { FocalLengths } from '@/focal';
import { Lenses, createLensKey } from '@/lens'; import { Lenses, createLensKey } from '@/lens';
import { migrationForError } from './migration'; import { migrationForError } from './migration';
import { UPDATED_BEFORE_01, UPDATED_BEFORE_02 } from '../outdated';
import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
const createPhotosTable = () => const createPhotosTable = () =>
sql` sql`
@ -445,3 +447,39 @@ export const getPhoto = async (
.then(({ rows }) => rows.map(parsePhotoFromDb)) .then(({ rows }) => rows.map(parsePhotoFromDb))
.then(photos => photos.length > 0 ? photos[0] : undefined); .then(photos => photos.length > 0 ? photos[0] : undefined);
}, 'getPhoto'); }, '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',
);

View File

@ -22,8 +22,6 @@ import { isBefore } from 'date-fns';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
export const OUTDATED_THRESHOLD = new Date('2024-06-16');
// INFINITE SCROLL: FEED // INFINITE SCROLL: FEED
export const INFINITE_SCROLL_FEED_INITIAL = export const INFINITE_SCROLL_FEED_INITIAL =
process.env.NODE_ENV === 'development' ? 2 : 12; process.env.NODE_ENV === 'development' ? 2 : 12;

13
src/photo/outdated.ts Normal file
View File

@ -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
);
};