diff --git a/src/photo/db/migration.ts b/src/photo/db/migration.ts new file mode 100644 index 00000000..eb29ee49 --- /dev/null +++ b/src/photo/db/migration.ts @@ -0,0 +1,40 @@ +import { sql } from '@/platforms/postgres'; + +interface Migration { + label: string + fields: string[] + run: () => ReturnType +} + +export const MIGRATIONS: Migration[] = [{ + label: '01: AI Text Generation', + fields: ['caption', 'semantic_description'], + run: () => sql` + ALTER TABLE photos + ADD COLUMN IF NOT EXISTS caption TEXT, + ADD COLUMN IF NOT EXISTS semantic_description TEXT + `, +}, { + label: '02: Lens Metadata', + fields: ['lens_make', 'lens_model'], + run: () => sql` + ALTER TABLE photos + ADD COLUMN IF NOT EXISTS lens_make VARCHAR(255), + ADD COLUMN IF NOT EXISTS lens_model VARCHAR(255) + `, +}, { + label: '03: Fujifilm Recipe', + fields: ['fujifilm_recipe'], + run: () => sql` + ALTER TABLE photos + ADD COLUMN IF NOT EXISTS fujifilm_recipe JSONB + `, +}]; + +export const migrationForError = (e: any) => + MIGRATIONS.find(migration => + migration.fields.some(field => + new RegExp(`column "${field}" of relation "photos" does not exist`, 'i') + .test(e.message), + ), + ); \ No newline at end of file diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index 1d783465..d339196b 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -23,6 +23,7 @@ import { import { getWheresFromOptions } from '.'; import { FocalLengths } from '@/focal'; import { Lenses, createLensKey } from '@/lens'; +import { migrationForError } from './migration'; const createPhotosTable = () => sql` @@ -50,6 +51,7 @@ const createPhotosTable = () => latitude DOUBLE PRECISION, longitude DOUBLE PRECISION, film_simulation VARCHAR(255), + fujifilm_recipe JSONB, priority_order REAL, taken_at TIMESTAMP WITH TIME ZONE NOT NULL, taken_at_naive VARCHAR(255) NOT NULL, @@ -59,24 +61,6 @@ const createPhotosTable = () => ) `; -// Migration 01 -const MIGRATION_FIELDS_01 = ['caption', 'semantic_description']; -const runMigration01 = () => - sql` - ALTER TABLE photos - ADD COLUMN IF NOT EXISTS caption TEXT, - ADD COLUMN IF NOT EXISTS semantic_description TEXT - `; - -// Migration 02 -const MIGRATION_FIELDS_02 = ['lens_make', 'lens_model']; -const runMigration02 = () => - sql` - ALTER TABLE photos - ADD COLUMN IF NOT EXISTS lens_make VARCHAR(255), - ADD COLUMN IF NOT EXISTS lens_model VARCHAR(255) - `; - // Wrapper for most queries for JIT table creation/migration running const safelyQueryPhotos = async ( callback: () => Promise, @@ -89,19 +73,10 @@ const safelyQueryPhotos = async ( try { result = await callback(); } catch (e: any) { - if (MIGRATION_FIELDS_01.some(field => new RegExp( - `column "${field}" of relation "photos" does not exist`, - 'i', - ).test(e.message))) { - console.log('Running migration 01 ...'); - await runMigration01(); - result = await callback(); - } else if (MIGRATION_FIELDS_02.some(field => new RegExp( - `column "${field}" of relation "photos" does not exist`, - 'i', - ).test(e.message))) { - console.log('Running migration 02 ...'); - await runMigration02(); + const migration = migrationForError(e); + if (migration) { + console.log(`Running Migration ${migration.label} ...`); + await migration.run(); result = await callback(); } else if (/relation "photos" does not exist/i.test(e.message)) { // If the table does not exist, create it @@ -163,6 +138,7 @@ export const insertPhoto = (photo: PhotoDbInsert) => latitude, longitude, film_simulation, + fujifilm_recipe, priority_order, hidden, taken_at, @@ -192,6 +168,7 @@ export const insertPhoto = (photo: PhotoDbInsert) => ${photo.latitude}, ${photo.longitude}, ${photo.filmSimulation}, + ${JSON.stringify(photo.fujifilmRecipe)}, ${photo.priorityOrder}, ${photo.hidden}, ${photo.takenAt}, @@ -224,6 +201,7 @@ export const updatePhoto = (photo: PhotoDbInsert) => latitude=${photo.latitude}, longitude=${photo.longitude}, film_simulation=${photo.filmSimulation}, + fujifilm_recipe=${JSON.stringify(photo.fujifilmRecipe)}, priority_order=${photo.priorityOrder || null}, hidden=${photo.hidden}, taken_at=${photo.takenAt}, diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index c07997ee..5311e546 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -23,6 +23,7 @@ import { FilmSimulation } from '@/simulation'; import { GEO_PRIVACY_ENABLED } from '@/app/config'; import { TAG_FAVS, getValidationMessageForTags } from '@/tag'; import { MAKE_FUJIFILM } from '@/platforms/fujifilm'; +import { FujifilmRecipe } from '@/platforms/fujifilm/recipe'; type VirtualFields = 'favorite'; @@ -107,6 +108,11 @@ const FORM_METADATA = ( selectOptionsDefaultLabel: 'Unknown', shouldHide: ({ make }) => make !== MAKE_FUJIFILM, }, + fujifilmRecipe: { + type: 'textarea', + label: 'fujifilm recipe', + shouldHide: ({ make }) => make !== MAKE_FUJIFILM, + }, focalLength: { label: 'focal length' }, focalLengthIn35MmFormat: { label: 'focal length 35mm-equivalent' }, lensMake: { label: 'lens make' }, @@ -200,6 +206,7 @@ export const convertPhotoToFormData = ( export const convertExifToFormData = ( data: ExifData, filmSimulation?: FilmSimulation, + fujifilmRecipe?: Partial, ): Omit< Record, 'takenAt' | 'takenAtNaive' @@ -223,6 +230,7 @@ export const convertExifToFormData = ( longitude: !GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined, filmSimulation, + fujifilmRecipe: JSON.stringify(fujifilmRecipe), ...data.tags?.DateTimeOriginal && { takenAt: convertTimestampWithOffsetToPostgresString( data.tags.DateTimeOriginal, @@ -267,7 +275,10 @@ export const convertFormDataToPhotoDbInsert = ( }); return { - ...(photoForm as PhotoFormData & { filmSimulation?: FilmSimulation }), + ...(photoForm as PhotoFormData & { + filmSimulation?: FilmSimulation + fujifilmRecipe?: FujifilmRecipe + }), ...!photoForm.id && { id: generateNanoid() }, // Delete array field when empty tags: tags.length > 0 ? tags : undefined, diff --git a/src/photo/index.ts b/src/photo/index.ts index 83597475..47b35a68 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -20,6 +20,7 @@ import { parameterize } from '@/utility/string'; import camelcaseKeys from 'camelcase-keys'; 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'); @@ -66,6 +67,7 @@ export interface PhotoExif { latitude?: number longitude?: number filmSimulation?: FilmSimulation + fujifilmRecipe?: string takenAt?: string takenAtNaive?: string } @@ -88,11 +90,13 @@ export interface PhotoDbInsert extends PhotoExif { } // Raw db response -export interface PhotoDb extends Omit { +export interface PhotoDb extends + Omit { updatedAt: Date createdAt: Date takenAt: Date tags: string[] + fujifilmRecipe?: Partial } // Parsed db response @@ -159,6 +163,7 @@ export const convertPhotoToPhotoDbInsert = ( ): PhotoDbInsert => ({ ...photo, takenAt: photo.takenAt.toISOString(), + fujifilmRecipe: JSON.stringify(photo.fujifilmRecipe), }); export const photoStatsAsString = (photo: Photo) => [ diff --git a/src/photo/server.ts b/src/photo/server.ts index e9c94cf4..79d8fd76 100644 --- a/src/photo/server.ts +++ b/src/photo/server.ts @@ -83,7 +83,6 @@ export const extractImageDataFromBlobPath = async ( if (Buffer.isBuffer(makerNote)) { filmSimulation = getFujifilmSimulationFromMakerNote(makerNote); recipe = getFujifilmRecipeFromMakerNote(makerNote); - console.log(recipe); } } @@ -117,7 +116,7 @@ export const extractImageDataFromBlobPath = async ( url, }, ...generateBlurData && { blurData }, - ...convertExifToFormData(exifData, filmSimulation), + ...convertExifToFormData(exifData, filmSimulation, recipe), }, }, imageResizedBase64,