Consolidate outdated/needs AI text sync statuses

This commit is contained in:
Sam Becker 2025-04-19 15:00:24 -05:00
parent 00932b6687
commit f22d5f85a8
14 changed files with 123 additions and 137 deletions

View File

@ -1,11 +1,10 @@
import { getStoragePhotoUrlsNoStore } from '@/platforms/storage/cache'; import { getStoragePhotoUrlsNoStore } from '@/platforms/storage/cache';
import { getPhotos } from '@/photo/db/query'; import { getPhotos, getPhotosInNeedOfSyncCount } from '@/photo/db/query';
import { getPhotosMetaCached } from '@/photo/cache'; import { getPhotosMetaCached } from '@/photo/cache';
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';
import { import {
AI_TEXT_GENERATION_ENABLED, AI_TEXT_GENERATION_ENABLED,
PRESERVE_ORIGINAL_UPLOADS, PRESERVE_ORIGINAL_UPLOADS,
@ -35,7 +34,7 @@ export default async function AdminPhotosPage() {
getPhotosMetaCached({ hidden: 'include'}) getPhotosMetaCached({ hidden: 'include'})
.then(({ count }) => count) .then(({ count }) => count)
.catch(() => 0), .catch(() => 0),
getOutdatedPhotosCount() getPhotosInNeedOfSyncCount()
.catch(() => 0), .catch(() => 0),
DEBUG_PHOTO_BLOBS DEBUG_PHOTO_BLOBS
? getStoragePhotoUrlsNoStore() ? getStoragePhotoUrlsNoStore()

View File

@ -1,11 +1,11 @@
import AdminPhotosSyncClient from '@/admin/AdminPhotosSyncClient'; import AdminPhotosSyncClient from '@/admin/AdminPhotosSyncClient';
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config'; import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
import { getOutdatedPhotos } from '@/photo/db/query'; import { getPhotosInNeedOfSync } from '@/photo/db/query';
export const maxDuration = 60; export const maxDuration = 60;
export default async function AdminSyncPage() { export default async function AdminSyncPage() {
const photos = await getOutdatedPhotos() const photos = await getPhotosInNeedOfSync()
.catch(() => []); .catch(() => []);
return ( return (

View File

@ -78,7 +78,7 @@ export default function AdminPhotoMenu({
label: 'Sync', label: 'Sync',
labelComplex: <span className="inline-flex items-center gap-2"> labelComplex: <span className="inline-flex items-center gap-2">
<span>Sync</span> <span>Sync</span>
{photo.needsSync && {(photo.syncStatus.isOutdated || photo.syncStatus.isMissingAiText) &&
<InsightsIndicatorDot <InsightsIndicatorDot
colorOverride="blue" colorOverride="blue"
className="translate-y-[1.5px]" className="translate-y-[1.5px]"

View File

@ -13,6 +13,7 @@ import AdminUploadsTable from './AdminUploadsTable';
import { Timezone } from '@/utility/timezone'; import { Timezone } from '@/utility/timezone';
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';
import PhotoUploadWithStatus from '@/photo/PhotoUploadWithStatus'; import PhotoUploadWithStatus from '@/photo/PhotoUploadWithStatus';
import { pluralize } from '@/utility/string';
export default function AdminPhotosClient({ export default function AdminPhotosClient({
photos, photos,
@ -58,9 +59,10 @@ export default function AdminPhotosClient({
size={18} size={18}
className="translate-y-[-1px]" className="translate-y-[-1px]"
/>} />}
// TODO: Add tooltip tooltip={(
// TODO: Use LinkWithStatus pluralize(photosCountOutdated, 'photo') +
title={`${photosCountOutdated} Outdated Photos`} ' needs sync'
)}
className={clsx( className={clsx(
'text-blue-600 dark:text-blue-400', 'text-blue-600 dark:text-blue-400',
'border border-blue-200 dark:border-blue-800/60', 'border border-blue-200 dark:border-blue-800/60',

View File

@ -67,9 +67,7 @@ export default function AdminPhotosSyncClient({
> >
{arePhotoIdsSyncing {arePhotoIdsSyncing
? 'Syncing' ? 'Syncing'
: <ResponsiveText shortText={`Sync Next ${updateBatchSize}`}> : 'Sync All'}
Sync Next {updateBatchSize} Photos
</ResponsiveText>}
</LoaderButton>} </LoaderButton>}
> >
<div className="space-y-6"> <div className="space-y-6">

View File

@ -9,13 +9,13 @@ import {
} from '@/photo/db/query'; } from '@/photo/db/query';
import AdminAppInsightsClient from './AdminAppInsightsClient'; import AdminAppInsightsClient from './AdminAppInsightsClient';
import { getAllInsights, getGitHubMetaForCurrentApp } from '.'; import { getAllInsights, getGitHubMetaForCurrentApp } from '.';
import { getOutdatedPhotosCount } from '@/photo/db/query'; import { getPhotosInNeedOfSyncCount } from '@/photo/db/query';
export default async function AdminAppInsights() { export default async function AdminAppInsights() {
const [ const [
{ count: photosCount, dateRange }, { count: photosCount, dateRange },
{ count: photosCountHidden }, { count: photosCountHidden },
photosCountOutdated, photosCountNeedSync,
{ count: photosCountPortrait }, { count: photosCountPortrait },
codeMeta, codeMeta,
cameras, cameras,
@ -27,7 +27,7 @@ export default async function AdminAppInsights() {
] = await Promise.all([ ] = await Promise.all([
getPhotosMeta({ hidden: 'include' }), getPhotosMeta({ hidden: 'include' }),
getPhotosMeta({ hidden: 'only' }), getPhotosMeta({ hidden: 'only' }),
getOutdatedPhotosCount(), getPhotosInNeedOfSyncCount(),
getPhotosMeta({ maximumAspectRatio: 0.9 }), getPhotosMeta({ maximumAspectRatio: 0.9 }),
getGitHubMetaForCurrentApp(), getGitHubMetaForCurrentApp(),
getUniqueCameras(), getUniqueCameras(),
@ -44,14 +44,14 @@ export default async function AdminAppInsights() {
insights={getAllInsights({ insights={getAllInsights({
codeMeta, codeMeta,
photosCount, photosCount,
photosCountOutdated, photosCountNeedSync,
photosCountPortrait, photosCountPortrait,
tagsCount: tags.length, tagsCount: tags.length,
})} })}
photoStats={{ photoStats={{
photosCount, photosCount,
photosCountHidden, photosCountHidden,
photosCountOutdated, photosCountNeedSync,
camerasCount: cameras.length, camerasCount: cameras.length,
lensesCount: lenses.length, lensesCount: lenses.length,
tagsCount: tags.length, tagsCount: tags.length,

View File

@ -50,7 +50,7 @@ import { HiOutlineDocumentText } from 'react-icons/hi';
const DEBUG_COMMIT_SHA = '4cd29ed'; const DEBUG_COMMIT_SHA = '4cd29ed';
const DEBUG_COMMIT_MESSAGE = 'Long commit message for debugging purposes'; const DEBUG_COMMIT_MESSAGE = 'Long commit message for debugging purposes';
const DEBUG_BEHIND_BY = 9; const DEBUG_BEHIND_BY = 9;
const DEBUG_PHOTOS_COUNT_OUTDATED = 7; const DEBUG_PHOTOS_NEED_SYNC_COUNT = 7;
const TEXT_COLOR_WARNING = 'text-amber-600 dark:text-amber-500'; const TEXT_COLOR_WARNING = 'text-amber-600 dark:text-amber-500';
const TEXT_COLOR_BLUE = 'text-blue-600 dark:text-blue-500'; const TEXT_COLOR_BLUE = 'text-blue-600 dark:text-blue-500';
@ -91,7 +91,7 @@ export default function AdminAppInsightsClient({
photoStats: { photoStats: {
photosCount, photosCount,
photosCountHidden, photosCountHidden,
photosCountOutdated, photosCountNeedSync,
camerasCount, camerasCount,
lensesCount, lensesCount,
tagsCount, tagsCount,
@ -114,7 +114,7 @@ export default function AdminAppInsightsClient({
noAiRateLimiting, noAiRateLimiting,
noConfiguredDomain, noConfiguredDomain,
noConfiguredMeta, noConfiguredMeta,
outdatedPhotos, photosNeedSync,
photoMatting, photoMatting,
camerasFirst, camerasFirst,
gridFirst, gridFirst,
@ -417,7 +417,7 @@ export default function AdminAppInsightsClient({
</AdminEmptyState>} </AdminEmptyState>}
</ScoreCard> </ScoreCard>
<ScoreCard title="Library Stats"> <ScoreCard title="Library Stats">
{(outdatedPhotos || debug) && <ScoreCardRow {(photosNeedSync || debug) && <ScoreCardRow
icon={<LiaBroomSolid icon={<LiaBroomSolid
size={19} size={19}
className={clsx( className={clsx(
@ -427,7 +427,7 @@ export default function AdminAppInsightsClient({
/>} />}
content={renderHighlightText( content={renderHighlightText(
pluralize( pluralize(
photosCountOutdated || DEBUG_PHOTOS_COUNT_OUTDATED, photosCountNeedSync || DEBUG_PHOTOS_NEED_SYNC_COUNT,
'photo', 'photo',
) + ' need to be synced', ) + ' need to be synced',
'blue', 'blue',

View File

@ -39,7 +39,7 @@ const _INSIGHTS_TEMPLATE = [
type AdminAppInsightRecommendation = typeof _INSIGHTS_TEMPLATE[number]; type AdminAppInsightRecommendation = typeof _INSIGHTS_TEMPLATE[number];
const _INSIGHTS_LIBRARY = [ const _INSIGHTS_LIBRARY = [
'outdatedPhotos', 'photosNeedSync',
] as const; ] as const;
type AdminAppInsightLibrary = typeof _INSIGHTS_LIBRARY[number]; type AdminAppInsightLibrary = typeof _INSIGHTS_LIBRARY[number];
@ -58,7 +58,7 @@ export const hasTemplateRecommendations = (insights: AdminAppInsights) =>
export interface PhotoStats { export interface PhotoStats {
photosCount: number photosCount: number
photosCountHidden: number photosCountHidden: number
photosCountOutdated: number photosCountNeedSync: number
camerasCount: number camerasCount: number
lensesCount: number lensesCount: number
tagsCount: number tagsCount: number
@ -80,10 +80,10 @@ export const getGitHubMetaForCurrentApp = () =>
export const getSignificantInsights = ({ export const getSignificantInsights = ({
codeMeta, codeMeta,
photosCountOutdated, photosCountNeedSync,
}: { }: {
codeMeta: Awaited<ReturnType<typeof getGitHubMetaForCurrentApp>> codeMeta: Awaited<ReturnType<typeof getGitHubMetaForCurrentApp>>
photosCountOutdated: number photosCountNeedSync: number
}) => { }) => {
const { const {
isAiTextGenerationEnabled, isAiTextGenerationEnabled,
@ -95,7 +95,7 @@ export const getSignificantInsights = ({
forkBehind: Boolean(codeMeta?.isBehind), forkBehind: Boolean(codeMeta?.isBehind),
noAiRateLimiting: isAiTextGenerationEnabled && !hasRedisStorage, noAiRateLimiting: isAiTextGenerationEnabled && !hasRedisStorage,
noConfiguredDomain: !hasDomain, noConfiguredDomain: !hasDomain,
outdatedPhotos: Boolean(photosCountOutdated), photosNeedSync: Boolean(photosCountNeedSync),
}; };
}; };
@ -106,19 +106,19 @@ export const indicatorStatusForSignificantInsights = (
forkBehind, forkBehind,
noAiRateLimiting, noAiRateLimiting,
noConfiguredDomain, noConfiguredDomain,
outdatedPhotos, photosNeedSync,
} = insights; } = insights;
if (noAiRateLimiting || noConfiguredDomain) { if (noAiRateLimiting || noConfiguredDomain) {
return 'yellow'; return 'yellow';
} else if (forkBehind || outdatedPhotos) { } else if (forkBehind || photosNeedSync) {
return 'blue'; return 'blue';
} }
}; };
export const getAllInsights = ({ export const getAllInsights = ({
codeMeta, codeMeta,
photosCountOutdated, photosCountNeedSync,
photosCount, photosCount,
photosCountPortrait, photosCountPortrait,
tagsCount, tagsCount,
@ -127,7 +127,7 @@ export const getAllInsights = ({
photosCountPortrait: number photosCountPortrait: number
tagsCount: number tagsCount: number
}) => ({ }) => ({
...getSignificantInsights({ codeMeta, photosCountOutdated }), ...getSignificantInsights({ codeMeta, photosCountNeedSync }),
noFork: !codeMeta?.isForkedFromBase && !codeMeta?.isBaseRepo, noFork: !codeMeta?.isForkedFromBase && !codeMeta?.isBaseRepo,
noAi: !AI_TEXT_GENERATION_ENABLED, noAi: !AI_TEXT_GENERATION_ENABLED,
noConfiguredMeta: noConfiguredMeta:

View File

@ -1,4 +1,4 @@
import { getOutdatedPhotosCount } from '@/photo/db/query'; import { getPhotosInNeedOfSyncCount } from '@/photo/db/query';
import { import {
getSignificantInsights, getSignificantInsights,
indicatorStatusForSignificantInsights, indicatorStatusForSignificantInsights,
@ -8,15 +8,15 @@ import { getGitHubMetaForCurrentApp } from '.';
export const getInsightsIndicatorStatus = async () => { export const getInsightsIndicatorStatus = async () => {
const [ const [
codeMeta, codeMeta,
photosCountOutdated, photosCountNeedSync,
] = await Promise.all([ ] = await Promise.all([
getGitHubMetaForCurrentApp(), getGitHubMetaForCurrentApp(),
getOutdatedPhotosCount(), getPhotosInNeedOfSyncCount(),
]); ]);
const significantInsights = getSignificantInsights({ const significantInsights = getSignificantInsights({
codeMeta, codeMeta,
photosCountOutdated, photosCountNeedSync,
}); });
return indicatorStatusForSignificantInsights(significantInsights); return indicatorStatusForSignificantInsights(significantInsights);

View File

@ -2,7 +2,6 @@ import { BiCopy } from 'react-icons/bi';
import LoaderButton from './primitives/LoaderButton'; import LoaderButton from './primitives/LoaderButton';
import clsx from 'clsx/lite'; import clsx from 'clsx/lite';
import { toastSuccess } from '@/toast'; import { toastSuccess } from '@/toast';
import Tooltip from './Tooltip';
import { ComponentProps } from 'react'; import { ComponentProps } from 'react';
export default function CopyButton({ export default function CopyButton({
@ -10,20 +9,18 @@ export default function CopyButton({
text, text,
subtle, subtle,
iconSize = 15, iconSize = 15,
tooltip,
tooltipColor,
className, className,
...props
}: { }: {
label: string label: string
text?: string, text?: string,
subtle?: boolean subtle?: boolean
iconSize?: number iconSize?: number
tooltip?: string
tooltipColor?: ComponentProps<typeof Tooltip>['color']
className?: string className?: string
}) { } & ComponentProps<typeof LoaderButton>) {
const button = return (
<LoaderButton <LoaderButton
{...props}
icon={<BiCopy size={iconSize} />} icon={<BiCopy size={iconSize} />}
className={clsx( className={clsx(
subtle && 'text-gray-300 dark:text-gray-700', subtle && 'text-gray-300 dark:text-gray-700',
@ -37,13 +34,6 @@ export default function CopyButton({
: undefined} : undefined}
styleAs="link" styleAs="link"
disabled={!text} disabled={!text}
/>; />
return (
tooltip
? <Tooltip content={tooltip} color={tooltipColor}>
{button}
</Tooltip>
: button
); );
} }

View File

@ -2,21 +2,10 @@
import Spinner, { SpinnerColor } from '@/components/Spinner'; import Spinner, { SpinnerColor } from '@/components/Spinner';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import { ButtonHTMLAttributes, ReactNode } from 'react'; import { ButtonHTMLAttributes, ComponentProps, ReactNode } from 'react';
import Tooltip from '../Tooltip';
export default function LoaderButton(props: { export default function LoaderButton({
isLoading?: boolean
icon?: ReactNode
spinnerColor?: SpinnerColor
spinnerClassName?: string
styleAs?: 'button' | 'link' | 'link-without-hover'
hideTextOnMobile?: boolean
confirmText?: string
shouldPreventDefault?: boolean
primary?: boolean
hideFocusOutline?: boolean
} & ButtonHTMLAttributes<HTMLButtonElement>) {
const {
children, children,
isLoading, isLoading,
icon, icon,
@ -32,10 +21,24 @@ export default function LoaderButton(props: {
onClick, onClick,
disabled, disabled,
className, className,
tooltip,
tooltipColor,
...rest ...rest
} = props; }: {
isLoading?: boolean
return ( icon?: ReactNode
spinnerColor?: SpinnerColor
spinnerClassName?: string
styleAs?: 'button' | 'link' | 'link-without-hover'
hideTextOnMobile?: boolean
confirmText?: string
shouldPreventDefault?: boolean
primary?: boolean
hideFocusOutline?: boolean
tooltip?: string
tooltipColor?: ComponentProps<typeof Tooltip>['color']
} & ButtonHTMLAttributes<HTMLButtonElement>) {
const button =
<button <button
{...rest} {...rest}
type={type} type={type}
@ -86,6 +89,13 @@ export default function LoaderButton(props: {
)}> )}>
{children} {children}
</span>} </span>}
</button> </button>;
return (
tooltip
? <Tooltip content={tooltip} color={tooltipColor}>
{button}
</Tooltip>
: button
); );
} }

View File

@ -579,44 +579,22 @@ export const getPhoto = async (
// Sync queries // Sync queries
const outdatedWhereClause = const outdatedWhereClauses = [
`WHERE updated_at < $1 OR (updated_at < $2 AND make = $3)`; `updated_at < $1`,
`(updated_at < $2 AND make = $3)`,
];
const outdatedValues = [ const outdatedWhereValues = [
UPDATED_BEFORE_01.toISOString(), UPDATED_BEFORE_01.toISOString(),
UPDATED_BEFORE_02.toISOString(), UPDATED_BEFORE_02.toISOString(),
MAKE_FUJIFILM, MAKE_FUJIFILM,
]; ];
export const getOutdatedPhotos = () => safelyQueryPhotos( const needsAiTextWhereClauses = (
() => query(`
SELECT * FROM photos
${outdatedWhereClause}
ORDER BY created_at DESC
LIMIT ${SYNC_QUERY_LIMIT}
`,
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',
);
const photosThatNeedAiTextWhereClause = (
AI_TEXT_GENERATION_ENABLED && AI_TEXT_GENERATION_ENABLED &&
AI_TEXT_AUTO_GENERATED_FIELDS.length AI_TEXT_AUTO_GENERATED_FIELDS.length
) )
? 'WHERE ' + AI_TEXT_AUTO_GENERATED_FIELDS ? AI_TEXT_AUTO_GENERATED_FIELDS
.map(field => { .map(field => {
switch (field) { switch (field) {
case 'title': return `(title <> '') IS NOT TRUE`; case 'title': return `(title <> '') IS NOT TRUE`;
@ -624,29 +602,32 @@ const photosThatNeedAiTextWhereClause = (
case 'tags': return `array_length(tags, 1) = 0`; case 'tags': return `array_length(tags, 1) = 0`;
case 'semantic': return `(semantic_description <> '') IS NOT TRUE`; case 'semantic': return `(semantic_description <> '') IS NOT TRUE`;
} }
}).join(' OR ') })
: undefined; : [];
export const getPhotosThatNeedAiText = () => safelyQueryPhotos( const needsSyncWhereStatement =
async () => photosThatNeedAiTextWhereClause `WHERE ${outdatedWhereClauses.concat(needsAiTextWhereClauses).join(' OR ')}`;
? query(`
export const getPhotosInNeedOfSync = () => safelyQueryPhotos(
() => query(`
SELECT * FROM photos SELECT * FROM photos
${photosThatNeedAiTextWhereClause} ${needsSyncWhereStatement}
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT ${SYNC_QUERY_LIMIT} LIMIT ${SYNC_QUERY_LIMIT}
`) `,
.then(({ rows }) => rows.map(parsePhotoFromDb)) outdatedWhereValues,
: [] as Photo[], )
'getPhotosThatNeedAiText', .then(({ rows }) => rows.map(parsePhotoFromDb)),
'getPhotosInNeedOfSync',
); );
export const getPhotosThatNeedAiTextCount = () => safelyQueryPhotos( export const getPhotosInNeedOfSyncCount = () => safelyQueryPhotos(
async () => photosThatNeedAiTextWhereClause () => query(`
? query(`
SELECT COUNT(*) FROM photos SELECT COUNT(*) FROM photos
${photosThatNeedAiTextWhereClause} ${needsSyncWhereStatement}
`) `,
.then(({ rows }) => parseInt(rows[0].count, 10)) outdatedWhereValues,
: 0, )
'getPhotosThatNeedAiTextCount', .then(({ rows }) => parseInt(rows[0].count, 10)),
'getPhotosInNeedOfSyncCount',
); );

View File

@ -23,7 +23,7 @@ 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';
import { FujifilmSimulation } from '@/platforms/fujifilm/simulation'; import { FujifilmSimulation } from '@/platforms/fujifilm/simulation';
import { doesPhotoNeedSync } from './sync'; import { PhotoSyncStatus, generatePhotoSyncStatus } from './sync';
// INFINITE SCROLL: FEED // INFINITE SCROLL: FEED
export const INFINITE_SCROLL_FEED_INITIAL = export const INFINITE_SCROLL_FEED_INITIAL =
@ -97,7 +97,7 @@ export interface PhotoDb extends
updatedAt: Date updatedAt: Date
createdAt: Date createdAt: Date
takenAt: Date takenAt: Date
tags: string[] tags?: string[]
} }
// Parsed db response // Parsed db response
@ -109,15 +109,16 @@ export interface Photo extends Omit<PhotoDb, 'recipeData'> {
exposureTimeFormatted?: string exposureTimeFormatted?: string
exposureCompensationFormatted?: string exposureCompensationFormatted?: string
takenAtNaiveFormatted: string takenAtNaiveFormatted: string
tags: string[]
recipeData?: FujifilmRecipe recipeData?: FujifilmRecipe
needsSync?: boolean syncStatus: PhotoSyncStatus
} }
export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => { export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
const photoDb = camelcaseKeys( const photoDb = camelcaseKeys(
photoDbRaw as unknown as Record<string, unknown>, photoDbRaw as unknown as Record<string, unknown>,
) as unknown as PhotoDb; ) as unknown as PhotoDb;
const photo: Photo ={ return {
...photoDb, ...photoDb,
tags: photoDb.tags ?? [], tags: photoDb.tags ?? [],
focalLengthFormatted: focalLengthFormatted:
@ -140,9 +141,8 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
? JSON.parse(photoDb.recipeData) ? JSON.parse(photoDb.recipeData)
: photoDb.recipeData : photoDb.recipeData
: undefined, : undefined,
}; syncStatus: generatePhotoSyncStatus(photoDb),
photo.needsSync = doesPhotoNeedSync(photo); } as Photo;
return photo;
}; };
export const parseCachedPhotoDates = (photo: Photo) => ({ export const parseCachedPhotoDates = (photo: Photo) => ({

View File

@ -1,14 +1,19 @@
import { MAKE_FUJIFILM } from '@/platforms/fujifilm'; import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
import { Photo } from '.'; import { PhotoDb } from '.';
import { AI_TEXT_AUTO_GENERATED_FIELDS } from '@/app/config'; import { AI_TEXT_AUTO_GENERATED_FIELDS } from '@/app/config';
export interface PhotoSyncStatus {
isOutdated: boolean;
isMissingAiText: boolean;
}
export const SYNC_QUERY_LIMIT = 1000; export const SYNC_QUERY_LIMIT = 1000;
export const UPDATED_BEFORE_01 = new Date('2024-06-16'); export const UPDATED_BEFORE_01 = new Date('2024-06-16');
// UTC 2025-02-24 05:30:00 // UTC 2025-02-24 05:30:00
export const UPDATED_BEFORE_02 = new Date(Date.UTC(2025, 1, 24, 5, 30, 0)); export const UPDATED_BEFORE_02 = new Date(Date.UTC(2025, 1, 24, 5, 30, 0));
const isPhotoOutdated = (photo: Photo) => const isPhotoOutdated = (photo: PhotoDb) =>
photo.updatedAt < UPDATED_BEFORE_01 || ( photo.updatedAt < UPDATED_BEFORE_01 || (
photo.updatedAt < UPDATED_BEFORE_02 && photo.updatedAt < UPDATED_BEFORE_02 &&
photo.make === MAKE_FUJIFILM photo.make === MAKE_FUJIFILM
@ -19,12 +24,13 @@ const doesPhotoNeedAiText = ({
caption, caption,
tags = [], tags = [],
semanticDescription, semanticDescription,
}: Photo) => }: PhotoDb) =>
(AI_TEXT_AUTO_GENERATED_FIELDS.includes('title') && !title) || (AI_TEXT_AUTO_GENERATED_FIELDS.includes('title') && !title) ||
(AI_TEXT_AUTO_GENERATED_FIELDS.includes('caption') && !caption) || (AI_TEXT_AUTO_GENERATED_FIELDS.includes('caption') && !caption) ||
(AI_TEXT_AUTO_GENERATED_FIELDS.includes('tags') && tags.length === 0) || (AI_TEXT_AUTO_GENERATED_FIELDS.includes('tags') && tags.length === 0) ||
(AI_TEXT_AUTO_GENERATED_FIELDS.includes('semantic') && !semanticDescription); (AI_TEXT_AUTO_GENERATED_FIELDS.includes('semantic') && !semanticDescription);
export const doesPhotoNeedSync = (photo: Photo) => export const generatePhotoSyncStatus = (photo: PhotoDb): PhotoSyncStatus => ({
isPhotoOutdated(photo) || isOutdated: isPhotoOutdated(photo),
doesPhotoNeedAiText(photo); isMissingAiText: doesPhotoNeedAiText(photo),
});