@@ -45,11 +45,49 @@ interface InferredBatch extends PhotoBatch {
4545 isInferred : boolean ;
4646}
4747
48- export function assignPhotosToEvents (
48+ interface PatchMapping {
49+ [ timestamp : string ] : string ;
50+ }
51+
52+ const PATCH_FILE_PATH = path . join ( "scripts" , "import-data" , "patch_events.json" ) ;
53+
54+ async function loadPatchMappings ( ) : Promise < PatchMapping > {
55+ try {
56+ if ( existsSync ( PATCH_FILE_PATH ) ) {
57+ const content = await fs . readFile ( PATCH_FILE_PATH , "utf-8" ) ;
58+ return JSON . parse ( content ) ;
59+ }
60+ } catch ( err ) {
61+ logger . warn ( `Failed to load patch_events.json: ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
62+ }
63+ return { } ;
64+ }
65+
66+ async function savePatchMappings ( mappings : PatchMapping ) : Promise < void > {
67+ try {
68+ await fs . writeFile ( PATCH_FILE_PATH , JSON . stringify ( mappings , null , 2 ) ) ;
69+ logger . success ( `Saved patch mappings to ${ PATCH_FILE_PATH } ` ) ;
70+ } catch ( err ) {
71+ logger . error ( `Failed to save patch_events.json: ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
72+ }
73+ }
74+
75+ function findEventById ( eventId : string , eventsJSON : EventsWithVenuesJSON ) : Event | null {
76+ for ( const groupData of Object . values ( eventsJSON . groups ) ) {
77+ for ( const event of groupData . events ) {
78+ if ( event . id === eventId ) {
79+ return event ;
80+ }
81+ }
82+ }
83+ return null ;
84+ }
85+
86+ export async function assignPhotosToEvents (
4987 photosJSON : PhotoJSON ,
5088 eventsWithVenuesJSON : EventsWithVenuesJSON ,
5189 stats : ImportStatistics ,
52- ) : PhotoAssignmentResult {
90+ ) : Promise < PhotoAssignmentResult > {
5391 const photosByEvent : Record < string , Photo [ ] > = { } ;
5492 const photoBatchesByEvent : Record < string , InferredBatch [ ] > = { } ;
5593 let unassignedBatches = 0 ;
@@ -58,6 +96,12 @@ export function assignPhotosToEvents(
5896 stats . photoBatchesTotal = Object . keys ( photosJSON . groups ) . length ;
5997
6098 const inferredAssignments : Array < { eventId : string ; eventTitle : string ; timestamp : number } > = [ ] ;
99+
100+ // Load existing patch mappings
101+ const patchMappings = await loadPatchMappings ( ) ;
102+ const newPatchMappings : PatchMapping = { ...patchMappings } ;
103+ let patchMappingsUsed = 0 ;
104+ let newMappingsCreated = 0 ;
61105
62106 Object . entries ( photosJSON . groups ) . forEach ( ( [ key , grp ] ) => {
63107 if ( grp . event ) {
@@ -72,37 +116,70 @@ export function assignPhotosToEvents(
72116
73117 stats . photoBatchesAssigned ++ ;
74118 stats . photoBatchesUnchanged ++ ; // Confirmed assignments are unchanged
75- } else if ( INFER_EVENTS ) {
76- // Attempt to infer the event based on timestamp
77- const inferred = inferEventByTimestamp ( grp . timestamp , eventsWithVenuesJSON ) ;
78- if ( inferred ) {
79- inferredAssignments . push ( {
80- eventId : inferred . event . id ,
81- eventTitle : inferred . event . title ,
82- timestamp : grp . timestamp ,
83- } ) ;
84-
85- const list = photosByEvent [ inferred . event . id ] ?? [ ] ;
86- list . push ( ...grp . photos ) ;
87- photosByEvent [ inferred . event . id ] = list ;
88-
89- const batches = photoBatchesByEvent [ inferred . event . id ] ?? [ ] ;
90- batches . push ( { timestamp : grp . timestamp , photos : grp . photos , isInferred : true } ) ;
91- photoBatchesByEvent [ inferred . event . id ] = batches ;
92-
93- stats . photoBatchesAssigned ++ ;
94- stats . photoBatchesCreated ++ ; // Inferred assignments are new/created
119+ } else {
120+ // First check patch mappings (always, regardless of INFER_EVENTS)
121+ const timestampKey = grp . timestamp . toString ( ) ;
122+ const patchedEventId = patchMappings [ timestampKey ] ;
123+
124+ if ( patchedEventId ) {
125+ // Use patched mapping
126+ const event = findEventById ( patchedEventId , eventsWithVenuesJSON ) ;
127+ if ( event ) {
128+ const list = photosByEvent [ patchedEventId ] ?? [ ] ;
129+ list . push ( ...grp . photos ) ;
130+ photosByEvent [ patchedEventId ] = list ;
131+
132+ const batches = photoBatchesByEvent [ patchedEventId ] ?? [ ] ;
133+ batches . push ( { timestamp : grp . timestamp , photos : grp . photos , isInferred : false } ) ;
134+ photoBatchesByEvent [ patchedEventId ] = batches ;
135+
136+ stats . photoBatchesAssigned ++ ;
137+ stats . photoBatchesUnchanged ++ ;
138+ patchMappingsUsed ++ ;
139+ logger . debug ( `Used patch mapping for batch ${ timestampKey } → ${ patchedEventId } ` ) ;
140+ } else {
141+ logger . warn ( `Patch mapping references non-existent event ${ patchedEventId } for batch ${ timestampKey } ` ) ;
142+ unassignedBatches ++ ;
143+ stats . photoBatchesUnassigned ++ ;
144+ }
145+ } else if ( INFER_EVENTS ) {
146+ // Attempt to infer the event based on timestamp
147+ const inferred = inferEventByTimestamp ( grp . timestamp , eventsWithVenuesJSON ) ;
148+ if ( inferred ) {
149+ inferredAssignments . push ( {
150+ eventId : inferred . event . id ,
151+ eventTitle : inferred . event . title ,
152+ timestamp : grp . timestamp ,
153+ } ) ;
154+
155+ const list = photosByEvent [ inferred . event . id ] ?? [ ] ;
156+ list . push ( ...grp . photos ) ;
157+ photosByEvent [ inferred . event . id ] = list ;
158+
159+ const batches = photoBatchesByEvent [ inferred . event . id ] ?? [ ] ;
160+ batches . push ( { timestamp : grp . timestamp , photos : grp . photos , isInferred : true } ) ;
161+ photoBatchesByEvent [ inferred . event . id ] = batches ;
162+
163+ stats . photoBatchesAssigned ++ ;
164+ stats . photoBatchesCreated ++ ; // Inferred assignments are new/created
165+
166+ // Save this inference to the patch file only if not already defined
167+ if ( ! patchMappings [ timestampKey ] ) {
168+ newPatchMappings [ timestampKey ] = inferred . event . id ;
169+ newMappingsCreated ++ ;
170+ }
171+ } else {
172+ logger . warn ( `Could not infer event for photos batch with timestamp ${ grp . timestamp } ` ) ;
173+ unassignedBatches ++ ;
174+ stats . photoBatchesUnassigned ++ ;
175+ }
95176 } else {
96- logger . warn ( `Could not infer event for photos batch with timestamp ${ grp . timestamp } ` ) ;
177+ logger . debug (
178+ `Skipping photos batch ${ key } (timestamp: ${ grp . timestamp } ) because INFER_EVENTS is disabled and no patch mapping exists.` ,
179+ ) ;
97180 unassignedBatches ++ ;
98181 stats . photoBatchesUnassigned ++ ;
99182 }
100- } else {
101- logger . debug (
102- `Skipping photos batch ${ key } (timestamp: ${ grp . timestamp } ) because INFER_EVENTS is disabled and no event id present.` ,
103- ) ;
104- unassignedBatches ++ ;
105- stats . photoBatchesUnassigned ++ ;
106183 }
107184 } ) ;
108185
@@ -114,9 +191,19 @@ export function assignPhotosToEvents(
114191 stats ,
115192 ) ;
116193
194+ // Save updated patch mappings if we created new ones
195+ if ( newMappingsCreated > 0 ) {
196+ await savePatchMappings ( newPatchMappings ) ;
197+ logger . info ( `Created ${ newMappingsCreated } new patch mappings` ) ;
198+ }
199+
117200 // Combined reporting for photo assignments
118- if ( inferredAssignments . length > 0 || redistributions . length > 0 ) {
201+ if ( inferredAssignments . length > 0 || redistributions . length > 0 || patchMappingsUsed > 0 ) {
119202 logger . section ( "Photo Batch Processing" ) ;
203+
204+ if ( patchMappingsUsed > 0 ) {
205+ logger . info ( `Used ${ patchMappingsUsed } existing patch mappings` ) ;
206+ }
120207
121208 if ( inferredAssignments . length > 0 ) {
122209 logger . info ( `Inferred ${ inferredAssignments . length } photo batch assignments:` ) ;
0 commit comments