Import Instagram archive to a Bluesky account
9
fork

Configure Feed

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

Add time formatting and estimation functions

- Implement formatDuration to convert milliseconds to hours and minutes
- Create calculateEstimatedTime to estimate import duration
- Add logging for total import time
- Enhance main function with precise time tracking
- Add comprehensive unit tests for new time-related functions

+79 -26
+45 -25
src/app.ts
··· 5 5 import { BlueskyClient } from "./bluesky"; 6 6 import { processPost } from "./media"; 7 7 import { prepareVideoUpload, createVideoEmbed } from "./video"; 8 - import path from 'path'; 8 + import path from "path"; 9 9 10 10 dotenv.config(); 11 11 ··· 39 39 40 40 /** 41 41 * Returns the absolute path to the archive folder 42 - * @param TEST_VIDEO_MODE 43 - * @param TEST_IMAGE_MODE 44 - * @returns 42 + * @param TEST_VIDEO_MODE 43 + * @param TEST_IMAGE_MODE 44 + * @returns 45 45 */ 46 - export function getArchiveFolder(TEST_VIDEO_MODE: boolean, TEST_IMAGE_MODE: boolean) { 47 - const rootDir = path.resolve(__dirname, '..'); 48 - 49 - if (TEST_VIDEO_MODE) return path.join(rootDir, 'transfer/test_videos'); 50 - if (TEST_IMAGE_MODE) return path.join(rootDir, 'transfer/test_images'); 46 + export function getArchiveFolder( 47 + TEST_VIDEO_MODE: boolean, 48 + TEST_IMAGE_MODE: boolean 49 + ) { 50 + const rootDir = path.resolve(__dirname, ".."); 51 + 52 + if (TEST_VIDEO_MODE) return path.join(rootDir, "transfer/test_videos"); 53 + if (TEST_IMAGE_MODE) return path.join(rootDir, "transfer/test_images"); 51 54 return path.join(rootDir, process.env.ARCHIVE_FOLDER!); 52 55 } 53 56 ··· 95 98 * Validates test mode configuration 96 99 * @throws Error if both test modes are enabled 97 100 */ 98 - function validateTestConfig(TEST_VIDEO_MODE: boolean, TEST_IMAGE_MODE: boolean) { 101 + function validateTestConfig( 102 + TEST_VIDEO_MODE: boolean, 103 + TEST_IMAGE_MODE: boolean 104 + ) { 99 105 if (TEST_VIDEO_MODE && TEST_IMAGE_MODE) { 100 - throw new Error('Cannot enable both TEST_VIDEO_MODE and TEST_IMAGE_MODE simultaneously'); 106 + throw new Error( 107 + "Cannot enable both TEST_VIDEO_MODE and TEST_IMAGE_MODE simultaneously" 108 + ); 101 109 } 102 110 } 103 111 112 + export function formatDuration(milliseconds: number): string { 113 + const minutes = Math.floor(milliseconds / (1000 * 60)); 114 + const hours = Math.floor(minutes / 60); 115 + const remainingMinutes = minutes % 60; 116 + return `${hours} hours and ${remainingMinutes} minutes`; 117 + } 118 + 119 + export function calculateEstimatedTime(importedMedia: number): string { 120 + const estimatedMilliseconds = importedMedia * API_RATE_LIMIT_DELAY * 1.1; 121 + return formatDuration(estimatedMilliseconds); 122 + } 123 + 104 124 export async function main() { 105 125 // Set environment variables within function scope, allows mocked unit testing. 106 126 const SIMULATE = process.env.SIMULATE === "1"; ··· 117 137 : undefined; 118 138 const archivalFolder = getArchiveFolder(TEST_VIDEO_MODE, TEST_IMAGE_MODE); 119 139 120 - logger.info(`Import started at ${new Date().toISOString()}`); 140 + const importStart: Date = new Date(); 141 + logger.info(`Import started at ${importStart.toISOString()}`); 121 142 logger.info({ 122 143 SourceFolder: archivalFolder, 123 144 username: process.env.BLUESKY_USERNAME, ··· 141 162 142 163 let postsJsonPath: string; 143 164 if (TEST_VIDEO_MODE || TEST_IMAGE_MODE) { 144 - postsJsonPath = path.join(archivalFolder, 'posts.json'); 145 - logger.info(`--- TEST mode is enabled, using content from ${archivalFolder} ---`); 165 + postsJsonPath = path.join(archivalFolder, "posts.json"); 166 + logger.info( 167 + `--- TEST mode is enabled, using content from ${archivalFolder} ---` 168 + ); 146 169 } else { 147 - postsJsonPath = path.join(archivalFolder, 'your_instagram_activity/content/posts_1.json'); 170 + postsJsonPath = path.join( 171 + archivalFolder, 172 + "your_instagram_activity/content/posts_1.json" 173 + ); 148 174 } 149 175 150 176 const fInstaPosts = FS.readFileSync(postsJsonPath); ··· 288 314 logger.info(`Estimated time for real import: ${estimatedTime}`); 289 315 } 290 316 317 + const importEnd: Date = new Date(); 291 318 logger.info( 292 - `Import finished at ${new Date().toISOString()}, imported ${importedPosts} posts with ${importedMedia} media` 293 - ); 294 - } 295 - 296 - function calculateEstimatedTime(importedMedia: number): string { 297 - const minutes = Math.round( 298 - ((importedMedia * API_RATE_LIMIT_DELAY) / 1000 / 60) * 1.1 319 + `Import finished at ${importEnd.toISOString()}, imported ${importedPosts} posts with ${importedMedia} media` 299 320 ); 300 - const hours = Math.floor(minutes / 60); 301 - const min = minutes % 60; 302 - return `${hours} hours and ${min} minutes`; 321 + const totalTime = importEnd.getTime() - importStart.getTime(); 322 + logger.info(`Total import time: ${formatDuration(totalTime)}`); 303 323 }
+34 -1
tests/app.test.ts
··· 1 - import { main } from '../src/app'; 1 + import { main, formatDuration, calculateEstimatedTime } from '../src/app'; 2 2 import { BlueskyClient } from '../src/bluesky'; 3 3 import { processPost } from '../src/media'; 4 4 import { logger } from '../src/logger'; ··· 265 265 ); 266 266 }); 267 267 }); 268 + 269 + describe('Time Formatting Functions', () => { 270 + describe('formatDuration', () => { 271 + test('should format duration with hours and minutes', () => { 272 + const cases = [ 273 + { input: 3600000, expected: '1 hours and 0 minutes' }, // 1 hour 274 + { input: 5400000, expected: '1 hours and 30 minutes' }, // 1.5 hours 275 + { input: 900000, expected: '0 hours and 15 minutes' }, // 15 minutes 276 + { input: 7200000, expected: '2 hours and 0 minutes' }, // 2 hours 277 + { input: 8100000, expected: '2 hours and 15 minutes' }, // 2.25 hours 278 + ]; 279 + 280 + cases.forEach(({ input, expected }) => { 281 + expect(formatDuration(input)).toBe(expected); 282 + }); 283 + }); 284 + }); 285 + 286 + describe('calculateEstimatedTime', () => { 287 + test('should calculate estimated time based on media count', () => { 288 + // API_RATE_LIMIT_DELAY is 3000ms, with 1.1 multiplier 289 + const cases = [ 290 + { mediaCount: 20, expected: '0 hours and 1 minutes' }, // 20 * 3000 * 1.1 = 66000ms 291 + { mediaCount: 40, expected: '0 hours and 2 minutes' }, // 40 * 3000 * 1.1 = 132000ms 292 + { mediaCount: 10, expected: '0 hours and 0 minutes' }, // 10 * 3000 * 1.1 = 33000ms 293 + ]; 294 + 295 + cases.forEach(({ mediaCount, expected }) => { 296 + expect(calculateEstimatedTime(mediaCount)).toBe(expected); 297 + }); 298 + }); 299 + }); 300 + });