@@ -920,11 +920,85 @@ export const augmentationService = {
920920 return this . getLinkedInPhotoPath ( personId ) !== null ;
921921 } ,
922922
923+ getFamilySearchPhotoPath ( personId : string ) : string | null {
924+ // New standardized path with -familysearch suffix
925+ const jpgPath = path . join ( PHOTOS_DIR , `${ personId } -familysearch.jpg` ) ;
926+ const pngPath = path . join ( PHOTOS_DIR , `${ personId } -familysearch.png` ) ;
927+ if ( fs . existsSync ( jpgPath ) ) return jpgPath ;
928+ if ( fs . existsSync ( pngPath ) ) return pngPath ;
929+ // Legacy fallback: check for photos without suffix
930+ const legacyJpgPath = path . join ( PHOTOS_DIR , `${ personId } .jpg` ) ;
931+ const legacyPngPath = path . join ( PHOTOS_DIR , `${ personId } .png` ) ;
932+ if ( fs . existsSync ( legacyJpgPath ) ) return legacyJpgPath ;
933+ if ( fs . existsSync ( legacyPngPath ) ) return legacyPngPath ;
934+ return null ;
935+ } ,
936+
937+ hasFamilySearchPhoto ( personId : string ) : boolean {
938+ return this . getFamilySearchPhotoPath ( personId ) !== null ;
939+ } ,
940+
923941 parseLinkedInUrl ( url : string ) : string | null {
924942 const match = url . match ( / l i n k e d i n \. c o m \/ i n \/ ( [ A - Z a - z 0 - 9 _ - ] + ) / ) ;
925943 return match ? match [ 1 ] : null ;
926944 } ,
927945
946+ /**
947+ * Parse FamilySearch URL to extract the person ID
948+ * Format: https://www.familysearch.org/tree/person/details/XXXX-XXX
949+ */
950+ parseFamilySearchUrl ( url : string ) : string | null {
951+ const match = url . match ( / f a m i l y s e a r c h \. o r g \/ t r e e \/ p e r s o n \/ (?: d e t a i l s | v i t a l s | s o u r c e s | m e m o r i e s ) \/ ( [ A - Z 0 - 9 - ] + ) / i) ;
952+ return match ? match [ 1 ] : null ;
953+ } ,
954+
955+ /**
956+ * Link a FamilySearch profile to a person
957+ * This registers FamilySearch as an augmentation platform like other providers
958+ */
959+ async linkFamilySearch ( personId : string , familySearchUrl : string ) : Promise < PersonAugmentation > {
960+ logger . start ( 'augment' , `Linking FamilySearch for ${ personId } : ${ familySearchUrl } ` ) ;
961+
962+ const familySearchId = this . parseFamilySearchUrl ( familySearchUrl ) ;
963+ if ( ! familySearchId ) {
964+ throw new Error ( 'Invalid FamilySearch URL format. Expected: https://www.familysearch.org/tree/person/details/XXXX-XXX' ) ;
965+ }
966+
967+ // Get existing augmentation or create new
968+ const existing = this . getAugmentation ( personId ) || {
969+ id : personId ,
970+ platforms : [ ] ,
971+ photos : [ ] ,
972+ descriptions : [ ] ,
973+ updatedAt : new Date ( ) . toISOString ( ) ,
974+ } ;
975+
976+ // Add or update FamilySearch platform reference
977+ const existingPlatform = existing . platforms . find ( p => p . platform === 'familysearch' ) ;
978+ if ( existingPlatform ) {
979+ existingPlatform . url = familySearchUrl ;
980+ existingPlatform . externalId = familySearchId ;
981+ existingPlatform . linkedAt = new Date ( ) . toISOString ( ) ;
982+ } else {
983+ existing . platforms . push ( {
984+ platform : 'familysearch' ,
985+ url : familySearchUrl ,
986+ externalId : familySearchId ,
987+ linkedAt : new Date ( ) . toISOString ( ) ,
988+ } ) ;
989+ }
990+
991+ existing . updatedAt = new Date ( ) . toISOString ( ) ;
992+ this . saveAugmentation ( existing ) ;
993+
994+ // Also register external identity in SQLite
995+ registerExternalIdentityIfEnabled ( personId , 'familysearch' , familySearchId , familySearchUrl ) ;
996+
997+ logger . ok ( 'augment' , `Linked FamilySearch ${ familySearchId } to ${ personId } ` ) ;
998+
999+ return existing ;
1000+ } ,
1001+
9281002 async scrapeLinkedIn ( url : string ) : Promise < { headline ?: string ; company ?: string ; photoUrl ?: string ; profileId : string } > {
9291003 const profileId = this . parseLinkedInUrl ( url ) ;
9301004 if ( ! profileId ) {
@@ -1235,10 +1309,18 @@ export const augmentationService = {
12351309 }
12361310
12371311 // Check if we already have a photo from this platform locally - skip if we do
1238- const photoSuffix = platform === 'wikipedia' ? 'wiki' : platform === 'familysearch' ? '' : platform ;
1239- const jpgPath = photoSuffix ? path . join ( PHOTOS_DIR , `${ personId } -${ photoSuffix } .jpg` ) : path . join ( PHOTOS_DIR , `${ personId } .jpg` ) ;
1240- const pngPath = photoSuffix ? path . join ( PHOTOS_DIR , `${ personId } -${ photoSuffix } .png` ) : path . join ( PHOTOS_DIR , `${ personId } .png` ) ;
1241- const existingLocalPath = fs . existsSync ( jpgPath ) ? jpgPath : fs . existsSync ( pngPath ) ? pngPath : null ;
1312+ // All providers now use consistent suffixed naming: -{provider}
1313+ const photoSuffix = platform === 'wikipedia' ? 'wiki' : platform ;
1314+ const jpgPath = path . join ( PHOTOS_DIR , `${ personId } -${ photoSuffix } .jpg` ) ;
1315+ const pngPath = path . join ( PHOTOS_DIR , `${ personId } -${ photoSuffix } .png` ) ;
1316+ let existingLocalPath = fs . existsSync ( jpgPath ) ? jpgPath : fs . existsSync ( pngPath ) ? pngPath : null ;
1317+
1318+ // Legacy fallback for FamilySearch: check unsuffixed photos
1319+ if ( ! existingLocalPath && platform === 'familysearch' ) {
1320+ const legacyJpgPath = path . join ( PHOTOS_DIR , `${ personId } .jpg` ) ;
1321+ const legacyPngPath = path . join ( PHOTOS_DIR , `${ personId } .png` ) ;
1322+ existingLocalPath = fs . existsSync ( legacyJpgPath ) ? legacyJpgPath : fs . existsSync ( legacyPngPath ) ? legacyPngPath : null ;
1323+ }
12421324
12431325 if ( existingLocalPath ) {
12441326 logger . photo ( 'augment' , `Photo from ${ platform } already exists locally: ${ existingLocalPath } ` ) ;
@@ -1265,9 +1347,15 @@ export const augmentationService = {
12651347
12661348 // Special case for FamilySearch - use the already-scraped photo
12671349 if ( platform === 'familysearch' ) {
1268- const fsJpgPath = path . join ( PHOTOS_DIR , `${ personId } .jpg` ) ;
1269- const fsPngPath = path . join ( PHOTOS_DIR , `${ personId } .png` ) ;
1270- const fsPhotoPath = fs . existsSync ( fsJpgPath ) ? fsJpgPath : fs . existsSync ( fsPngPath ) ? fsPngPath : null ;
1350+ // Check new suffixed path first, then legacy unsuffixed path
1351+ const fsJpgPath = path . join ( PHOTOS_DIR , `${ personId } -familysearch.jpg` ) ;
1352+ const fsPngPath = path . join ( PHOTOS_DIR , `${ personId } -familysearch.png` ) ;
1353+ const legacyJpgPath = path . join ( PHOTOS_DIR , `${ personId } .jpg` ) ;
1354+ const legacyPngPath = path . join ( PHOTOS_DIR , `${ personId } .png` ) ;
1355+ const fsPhotoPath = fs . existsSync ( fsJpgPath ) ? fsJpgPath :
1356+ fs . existsSync ( fsPngPath ) ? fsPngPath :
1357+ fs . existsSync ( legacyJpgPath ) ? legacyJpgPath :
1358+ fs . existsSync ( legacyPngPath ) ? legacyPngPath : null ;
12711359
12721360 if ( ! fsPhotoPath ) {
12731361 throw new Error ( 'No FamilySearch photo available for this person' ) ;
0 commit comments