@@ -32,6 +32,8 @@ import {
3232 FacebookJobRow ,
3333 convertFacebookJobRowToFacebookJob ,
3434 FacebookArchivePost ,
35+ FacebookArchiveMedia ,
36+ FacebookPostWithMedia ,
3537 FacebookPostRow
3638} from './types'
3739import * as FacebookArchiveTypes from '../../archive-static-sites/facebook-archive/src/types' ;
@@ -180,6 +182,22 @@ export class FacebookAccountController {
180182 `ALTER TABLE post_new RENAME TO post;`
181183 ]
182184 } ,
185+ {
186+ name : "20250302_add_media_table" ,
187+ sql : [
188+ `CREATE TABLE post_media (
189+ id INTEGER PRIMARY KEY AUTOINCREMENT,
190+ mediaId TEXT NOT NULL UNIQUE,
191+ postId TEXT NOT NULL,
192+ type TEXT NOT NULL,
193+ uri TEXT NOT NULL,
194+ description TEXT,
195+ createdAt DATETIME,
196+ addedToDatabaseAt DATETIME NOT NULL,
197+ FOREIGN KEY(postId) REFERENCES post(postID)
198+ );`
199+ ]
200+ } ,
183201 ] )
184202 log . info ( "FacebookAccountController.initDB: database initialized" ) ;
185203 }
@@ -234,29 +252,60 @@ export class FacebookAccountController {
234252 }
235253
236254 log . info ( "FacebookAccountController.archiveBuild: building archive" ) ;
237-
238- // Posts
239- const posts : FacebookPostRow [ ] = exec (
255+ // Posts with optional media
256+ const postsFromDb = exec (
240257 this . db ,
241- "SELECT * FROM post ORDER BY createdAt DESC" ,
258+ `SELECT
259+ p.*,
260+ CASE
261+ WHEN pm.mediaId IS NOT NULL
262+ THEN GROUP_CONCAT(
263+ json_object(
264+ 'mediaId', pm.mediaId,
265+ 'postId', pm.postId,
266+ 'type', pm.type,
267+ 'uri', pm.uri,
268+ 'description', pm.description,
269+ 'createdAt', pm.createdAt,
270+ 'addedToDatabaseAt', pm.addedToDatabaseAt
271+ )
272+ )
273+ ELSE NULL
274+ END as media
275+ FROM post p
276+ LEFT JOIN post_media pm ON p.postID = pm.postId
277+ GROUP BY p.postID
278+ ORDER BY p.createdAt DESC` ,
242279 [ ] ,
243280 "all"
244- ) as FacebookPostRow [ ] ;
281+ ) ;
282+ // Transform into FacebookPostWithMedia
283+ const posts : FacebookPostWithMedia [ ] = ( postsFromDb as Array < FacebookPostRow & { media ?: string } > ) . map ( ( post ) => ( {
284+ ...post ,
285+ media : post . media ? JSON . parse ( `[${ post . media } ]` ) : undefined
286+ } ) ) ;
245287
246288 // Get the current account's userID
247289 // const accountUser = users.find((user) => user.screenName == this.account?.username);
248290 // const accountUserID = accountUser?.userID;
249291
250- const postRowToArchivePost = ( post : FacebookPostRow ) : FacebookArchiveTypes . Post => {
292+ const postRowToArchivePost = ( post : FacebookPostWithMedia ) : FacebookArchiveTypes . Post => {
251293 const archivePost : FacebookArchiveTypes . Post = {
252294 postID : post . postID ,
253295 createdAt : post . createdAt ,
254296 text : post . text ,
255297 title : post . title ,
256298 isReposted : post . isReposted ,
257299 archivedAt : post . archivedAt ,
300+ media : post . media ?. map ( m => ( {
301+ mediaId : m . mediaId ,
302+ type : m . type ,
303+ uri : m . uri ,
304+ description : m . description ,
305+ createdAt : m . createdAt
306+ } ) )
258307 } ;
259- return archivePost
308+ return archivePost ;
260309 }
261310
262311 // Build the archive object
@@ -524,30 +573,46 @@ export class FacebookAccountController {
524573 const posts = JSON . parse ( postsFile ) ;
525574
526575 for ( const post of posts ) {
527- // Skip if no post text
528576 const postText = post . data ?. find ( ( d : { post ?: string } ) => 'post' in d && typeof d . post === 'string' ) ?. post ;
529- if ( ! postText ) {
530- log . info ( "FacebookAccountController.importFacebookArchive: skipping post with no text" ) ;
531- continue ;
532- }
533577
534578 // Check if it's a shared post by looking for external_context in attachments
535579 const isSharedPost = post . attachments ?. [ 0 ] ?. data ?. [ 0 ] ?. external_context !== undefined ;
536580 log . info ( "FacebookAccountController.importFacebookArchive: isSharedPost" , isSharedPost ) ;
537581
538- // Skip if it's a group post, shares a group, etc. We will extend the import logic
539- // to include other data types in the future.
540- if ( post . attachments && ! isSharedPost ) {
541- log . info ( "FacebookAccountController.importFacebookArchive: skipping unknown post type" ) ;
542- continue ;
582+ // Check if it's a share of a group post
583+ const isGroupPost = post . attachments ?. [ 0 ] ?. data ?. [ 0 ] ?. name !== undefined ;
584+ const groupName = isGroupPost ? post . attachments [ 0 ] . data [ 0 ] . name : undefined ;
585+
586+ // For group posts, if there's no explicit post text, use the group name
587+ const finalText = isGroupPost
588+ ? ( postText || `Shared the group: ${ groupName } ` )
589+ : postText ;
590+
591+ // Process media attachments
592+ const media : FacebookArchiveMedia [ ] = [ ] ;
593+ if ( post . attachments ) {
594+ for ( const attachment of post . attachments ) {
595+ for ( const data of attachment . data ) {
596+ if ( data . media ) {
597+ media . push ( {
598+ uri : data . media . uri ,
599+ type : data . media . uri . endsWith ( '.mp4' ) ? 'video' : 'photo' ,
600+ description : data . media . description ,
601+ creationTimestamp : data . media . creation_timestamp
602+ } ) ;
603+ }
604+ }
605+ }
543606 }
607+ log . info ( "FacebookAccountController.importFacebookArchive: media" , media ) ;
544608
545609 postsData . push ( {
546610 id_str : post . timestamp . toString ( ) ,
547611 title : post . title || '' ,
548- full_text : postText ,
612+ full_text : finalText ,
549613 created_at : new Date ( post . timestamp * 1000 ) . toISOString ( ) ,
550- isReposted : isSharedPost ,
614+ isReposted : isSharedPost || isGroupPost , // Group shares are reposts too
615+ media : media . length > 0 ? media : undefined ,
551616 } ) ;
552617 }
553618 } catch ( e ) {
@@ -561,15 +626,14 @@ export class FacebookAccountController {
561626
562627 // Loop through the posts and add them to the database
563628 try {
564- postsData . forEach ( ( post ) => {
629+ postsData . forEach ( async ( post ) => {
565630 // Is this post already there?
566631 const existingPost = exec ( this . db , 'SELECT * FROM post WHERE postID = ?' , [ post . id_str ] , "get" ) as FacebookPostRow ;
567632 if ( existingPost ) {
568633 // Delete the existing post to re-import
569634 exec ( this . db , 'DELETE FROM post WHERE postID = ?' , [ post . id_str ] ) ;
570635 }
571636
572- // TODO: implement media import for facebook
573637 // TODO: implement urls import for facebook
574638
575639 // Import it
@@ -581,9 +645,15 @@ export class FacebookAccountController {
581645 post . isReposted ? 1 : 0 ,
582646 new Date ( ) ,
583647 ] ) ;
648+
649+ if ( post . media && post . media . length > 0 ) {
650+ log . info ( "FacebookAccountController.importFacebookArchive: importing media for post" , post . id_str ) ;
651+ await this . importFacebookArchiveMedia ( post . id_str , post . media , archivePath ) ;
652+ }
584653 importCount ++ ;
585654 } ) ;
586655 } catch ( e ) {
656+ log . error ( "FacebookAccountController.importFacebookArchive: error importing posts" , e ) ;
587657 return {
588658 status : "error" ,
589659 errorMessage : "Error importing posts: " + e ,
@@ -608,4 +678,37 @@ export class FacebookAccountController {
608678 skipCount : skipCount ,
609679 } ;
610680 }
681+
682+ async importFacebookArchiveMedia ( postId : string , media : FacebookArchiveMedia [ ] , archivePath : string ) : Promise < void > {
683+ for ( const mediaItem of media ) {
684+ const sourcePath = path . join ( archivePath , mediaItem . uri ) ;
685+ const mediaId = `${ postId } _${ path . basename ( mediaItem . uri ) } ` ;
686+
687+ // Create destination directory if it doesn't exist
688+ const mediaDir = path . join ( this . accountDataPath , 'media' ) ;
689+ if ( ! fs . existsSync ( mediaDir ) ) {
690+ fs . mkdirSync ( mediaDir , { recursive : true } ) ;
691+ }
692+
693+ const destPath = path . join ( mediaDir , path . basename ( mediaItem . uri ) ) ;
694+ try {
695+ await fs . promises . copyFile ( sourcePath , destPath ) ;
696+
697+ exec ( this . db ,
698+ 'INSERT INTO post_media (mediaId, postId, type, uri, description, createdAt, addedToDatabaseAt) VALUES (?, ?, ?, ?, ?, ?, ?)' ,
699+ [
700+ mediaId ,
701+ postId ,
702+ mediaItem . type ,
703+ path . basename ( mediaItem . uri ) ,
704+ mediaItem . description || null ,
705+ mediaItem . creationTimestamp ? new Date ( mediaItem . creationTimestamp * 1000 ) : null ,
706+ new Date ( )
707+ ]
708+ ) ;
709+ } catch ( error ) {
710+ log . error ( `FacebookAccountController.importFacebookArchiveMedia: Error importing media: ${ error } ` ) ;
711+ }
712+ }
713+ }
611714}
0 commit comments