Import Instagram archive to a Bluesky account
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

Gracefully handling missing reels file

- Move logic to utility function
- Add message for missing files
- Update unit tests

+157 -30
+18 -19
src/instagram-to-bluesky.test.ts
··· 1 - import fs from "fs"; 2 - 3 1 import { 4 2 main, 5 3 formatDuration, ··· 9 7 import { BlueskyClient } from "./bluesky/bluesky"; 10 8 import { ImagesEmbedImpl, VideoEmbedImpl } from "./bluesky/index"; 11 9 import { logger } from "./logger/logger"; 12 - import { InstagramMediaProcessor, ImageMediaProcessResultImpl } from "./media"; 10 + import { InstagramMediaProcessor, ImageMediaProcessResultImpl, readJsonFile } from "./media"; 13 11 14 12 import type { InstagramExportedPost } from "./media/InstagramExportedPost"; 15 13 ··· 66 64 process: mockProcess, 67 65 })), 68 66 decodeUTF8: jest.fn((x) => x), 67 + readJsonFile: jest.fn(), 69 68 ImageMediaProcessResultImpl: actual.ImageMediaProcessResultImpl, 70 69 VideoMediaProcessResultImpl: actual.VideoMediaProcessResultImpl 71 70 }; ··· 103 102 const mockReadFileSync = (mockValue) => { 104 103 return (path) => { 105 104 if (path.endsWith('reels.json')) { 106 - return JSON.stringify({"ig_reels_media": mockValue}) 105 + return JSON.parse(JSON.stringify({ "ig_reels_media": mockValue })) 107 106 } 108 - return JSON.stringify(mockValue) 107 + return JSON.parse(JSON.stringify(mockValue)); 109 108 } 110 109 }; 111 110 ··· 135 134 ], 136 135 }, 137 136 ]; 138 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync(mockValue)); 137 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync(mockValue)); 139 138 140 139 // Reset BlueskyClient mock 141 140 jest.mocked(BlueskyClient).mockClear(); ··· 172 171 ], 173 172 }; 174 173 175 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync([mockPost])); 174 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync([mockPost])); 176 175 177 176 await main(); 178 177 ··· 200 199 ], 201 200 }; 202 201 203 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync([oldPost])); 202 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync([oldPost])); 204 203 205 204 await main(); 206 205 ··· 223 222 ], 224 223 }; 225 224 226 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync([futurePost])); 225 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync([futurePost])); 227 226 228 227 await main(); 229 228 ··· 247 246 ], 248 247 }; 249 248 250 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync([exactMinDatePost])); 249 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync([exactMinDatePost])); 251 250 252 251 await main(); 253 252 ··· 277 276 ], 278 277 }; 279 278 280 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync([exactMaxDatePost])); 279 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync([exactMaxDatePost])); 281 280 282 281 await main(); 283 282 ··· 340 339 }, 341 340 ]; 342 341 343 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync(posts)); 342 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync(posts)); 344 343 345 344 await main(); 346 345 ··· 391 390 }, 392 391 ]; 393 392 394 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync(posts)); 393 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync(posts)); 395 394 396 395 await main(); 397 396 ··· 416 415 media: [{ title: "Invalid Media" }], 417 416 }; 418 417 419 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync([invalidPost])); 418 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync([invalidPost])); 420 419 421 420 await main(); 422 421 ··· 424 423 }); 425 424 426 425 test("should handle file reading errors", async () => { 427 - (fs.readFileSync as jest.Mock).mockImplementation(() => { 426 + (readJsonFile as jest.Mock).mockImplementation(() => { 428 427 throw new Error("File read error"); 429 428 }); 430 429 ··· 443 442 ], 444 443 }; 445 444 446 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync([mockPost])); 445 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync([mockPost])); 447 446 jest.mocked(BlueskyClient).prototype.createPost = jest 448 447 .fn() 449 448 .mockRejectedValue(new Error("Post failed")); ··· 469 468 ], 470 469 }; 471 470 472 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync([mockPost])); 471 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync([mockPost])); 473 472 474 473 await main(); 475 474 ··· 507 506 ], 508 507 }; 509 508 510 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync([mockPost])); 509 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync([mockPost])); 511 510 await main(); 512 511 513 512 expect(jest.mocked(BlueskyClient)).toHaveBeenCalled(); ··· 550 549 ], 551 550 }; 552 551 553 - (fs.readFileSync as jest.Mock).mockImplementation(mockReadFileSync([mockPost])); 552 + (readJsonFile as jest.Mock).mockImplementation(mockReadFileSync([mockPost])); 554 553 555 554 const embeddedMedia = mockPost.media.map(() => ({ 556 555 getType: () => "image",
+12 -9
src/instagram-to-bluesky.ts
··· 1 - import FS from "fs"; 2 1 import path from "path"; 3 2 4 3 import { BlobRef } from "@atproto/api"; ··· 20 19 decodeUTF8, 21 20 InstagramMediaProcessor, 22 21 InstagramExportedPost, 22 + readJsonFile, 23 23 } from "./media"; 24 24 25 25 const API_RATE_LIMIT_DELAY = 3000; // https://docs.bsky.app/docs/advanced-guides/rate-limits ··· 171 171 ); 172 172 } 173 173 174 - // Read instagram posts JSON file as raw buffer data. 175 - const instaPostsFileBuffer: Buffer = FS.readFileSync(postsJsonPath); 176 - const instaReelsFileBuffer: Buffer = FS.readFileSync(reelsJsonPath); 174 + // Read posts and reels data 175 + const instaPostsData = readJsonFile(postsJsonPath, 'No posts found. The file path may have changed - please update the env to point to the new folder containing posts_1.json'); 176 + const reelsJsonData = readJsonFile(reelsJsonPath, 'No reels found. Some accounts don\'t have reels, or the folder may have changed.'); 177 177 178 - // Decode raw JSON data into an object. 179 - const allInstaPosts: InstagramExportedPost[] = decodeUTF8([].concat( 180 - JSON.parse(instaPostsFileBuffer.toString()), 181 - JSON.parse(instaReelsFileBuffer.toString())['ig_reels_media'] 182 - )); 178 + // Extract reels data (some users don't have reels) 179 + const instaReelsData = reelsJsonData['ig_reels_media'] || []; 180 + 181 + // Decode raw JSON data into an object 182 + const allInstaPosts: InstagramExportedPost[] = decodeUTF8([ 183 + ...instaPostsData, 184 + ...instaReelsData 185 + ]); 183 186 184 187 // Initialize counters for posts and media. 185 188 let importedPosts = 0;
+100 -1
src/media/utils.test.ts
··· 1 - import { decodeUTF8 } from "./utils"; 1 + import FS from "fs"; 2 + 3 + import { decodeUTF8, readJsonFile } from "./utils"; 4 + import { logger } from "../logger/logger"; 2 5 3 6 describe("decodeUTF8", () => { 4 7 test("should decode Instagram Unicode escape sequences", () => { ··· 8 11 expect(result).toBe("Basil, Eucalyptus, Thyme 😍🌱"); 9 12 }); 10 13 }); 14 + 15 + jest.mock("../logger/logger", () => ({ 16 + logger: { 17 + info: jest.fn(), 18 + warn: jest.fn(), 19 + error: jest.fn(), 20 + debug: jest.fn(), 21 + }, 22 + })); 23 + 24 + // Mock the file system 25 + jest.mock("fs", () => ({ 26 + existsSync: jest.fn(), 27 + readFileSync: jest.fn(), 28 + })); 29 + 30 + describe("readJsonFile", () => { 31 + 32 + afterEach(() => { 33 + jest.resetAllMocks(); 34 + }); 35 + 36 + test("should log message if file does not exist", () => { 37 + // Arrange 38 + const filePath = '/nonexistent/file.json'; 39 + const customMessage = 'Custom missing file message'; 40 + (FS.existsSync as jest.Mock).mockReturnValue(false); 41 + 42 + // Act 43 + readJsonFile(filePath, customMessage); 44 + 45 + // Assert 46 + expect(logger.info).toHaveBeenCalledWith(customMessage); 47 + }); 48 + 49 + test("should return an empty array when file does not exist", () => { 50 + // Arrange 51 + const filePath = '/nonexistent/file.json'; 52 + (FS.existsSync as jest.Mock).mockReturnValue(false); 53 + 54 + // Act 55 + const result = readJsonFile(filePath); 56 + 57 + // Assert 58 + expect(result).toEqual([]); 59 + }); 60 + 61 + test("returns buffer json data", () => { 62 + // Arrange 63 + const filePath = '/existing/file.json'; 64 + const mockJsonData = [{ id: 1, title: 'Test Post' }]; 65 + const mockBuffer = Buffer.from(JSON.stringify(mockJsonData)); 66 + 67 + (FS.existsSync as jest.Mock).mockReturnValue(true); 68 + (FS.readFileSync as jest.Mock).mockReturnValue(mockBuffer); 69 + 70 + // Act 71 + const result = readJsonFile(filePath); 72 + 73 + // Assert 74 + expect(FS.readFileSync).toHaveBeenCalledWith(filePath); 75 + expect(result).toEqual(mockJsonData); 76 + expect(logger.info).not.toHaveBeenCalled(); 77 + }); 78 + 79 + test("should handle JSON parsing errors", () => { 80 + // Arrange 81 + const filePath = '/corrupted/file.json'; 82 + const mockBuffer = Buffer.from('invalid json'); 83 + 84 + (FS.existsSync as jest.Mock).mockReturnValue(true); 85 + (FS.readFileSync as jest.Mock).mockReturnValue(mockBuffer); 86 + 87 + // Act 88 + const result = readJsonFile(filePath); 89 + 90 + // Assert 91 + expect(logger.warn).toHaveBeenCalledWith( 92 + expect.stringContaining('Failed to parse /corrupted/file.json') 93 + ); 94 + expect(result).toEqual([]); 95 + }); 96 + 97 + test("should use custom fallback when file does not exist", () => { 98 + // Arrange 99 + const filePath = '/nonexistent/file.json'; 100 + const customFallback = [{ default: 'data' }]; 101 + (FS.existsSync as jest.Mock).mockReturnValue(false); 102 + 103 + // Act 104 + const result = readJsonFile(filePath, 'File missing', customFallback); 105 + 106 + // Assert 107 + expect(result).toEqual(customFallback); 108 + }); 109 + });
+27 -1
src/media/utils.ts
··· 79 79 } 80 80 81 81 return mediaBuffer; 82 - } 82 + } 83 + 84 + /** 85 + * Reads and parses a JSON file from the specified path. 86 + * 87 + * If the file does not exist, logs an informational message and returns the provided fallback value. 88 + * If the file exists but cannot be parsed as JSON, logs a warning and returns the fallback value. 89 + * 90 + * @param filePath - The path to the JSON file to read. 91 + * @param missingFileMessage - Optional message to log if the file is not found. Defaults to 'File not found.'. 92 + * @param fallback - Optional fallback value to return if the file is missing or cannot be parsed. Defaults to an empty array. 93 + * @returns The parsed JSON content as an array, or the fallback value if the file is missing or invalid. 94 + */ 95 + export function readJsonFile(filePath: string, missingFileMessage: string = 'File not found.', fallback: any[] = []): any[] { 96 + if (!FS.existsSync(filePath)) { 97 + logger.info(missingFileMessage) 98 + return fallback; 99 + } 100 + 101 + try { 102 + const buffer = FS.readFileSync(filePath); 103 + return JSON.parse(buffer.toString()); 104 + } catch (error) { 105 + logger.warn(`Failed to parse ${filePath}: ${(error as Error)?.message}`); 106 + return fallback; 107 + } 108 + };