Skip to content

Commit 1dce998

Browse files
committed
attribution links and many other things
1 parent 8529252 commit 1dce998

24 files changed

Lines changed: 1773 additions & 130 deletions

src/cli/args.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface CLIArgs {
2222
skipPixabay: boolean
2323
skipWikipedia: boolean
2424
skipGooglePlaces: boolean
25+
mediaLibraryPath: string | undefined
2526
quiet: boolean
2627
verbose: boolean
2728
dryRun: boolean
@@ -151,6 +152,10 @@ function parseCommaSeparated(value: unknown): string[] {
151152
.filter(Boolean)
152153
}
153154

155+
function parseOptionalString(value: unknown): string | undefined {
156+
return typeof value === 'string' ? value : undefined
157+
}
158+
154159
function parseConfigAction(action: string | undefined): 'list' | 'set' | 'unset' {
155160
if (action === 'set' || action === 'unset') {
156161
return action
@@ -173,30 +178,30 @@ function buildCLIArgs(commandName: string, input: string, opts: Record<string, u
173178
skipPixabay: opts.skipPixabay === true,
174179
skipWikipedia: opts.skipWikipedia === true,
175180
skipGooglePlaces: opts.skipGooglePlaces === true,
181+
mediaLibraryPath: parseOptionalString(opts.mediaLibraryPath),
176182
quiet: opts.quiet === true,
177183
verbose: opts.verbose === true,
178184
dryRun: opts.dryRun === true,
179185
debug: opts.debug === true,
180186
maxResults: Number.parseInt(String(opts.maxResults ?? '10'), 10),
181187
maxMessages: opts.maxMessages ? Number.parseInt(String(opts.maxMessages), 10) : undefined,
182188
method: parseMethod(opts.method),
183-
jsonOutput:
184-
opts.json === true ? 'stdout' : typeof opts.json === 'string' ? opts.json : undefined,
185-
homeCountry: typeof opts.homeCountry === 'string' ? opts.homeCountry : undefined,
186-
timezone: typeof opts.timezone === 'string' ? opts.timezone : undefined,
189+
jsonOutput: opts.json === true ? 'stdout' : parseOptionalString(opts.json),
190+
homeCountry: parseOptionalString(opts.homeCountry),
191+
timezone: parseOptionalString(opts.timezone),
187192
scrapeConcurrency: Number.parseInt(String(opts.concurrency ?? '5'), 10),
188193
scrapeTimeout: Number.parseInt(String(opts.timeout ?? '4000'), 10),
189194
noCache: opts.cache === false,
190-
cacheDir: typeof opts.cacheDir === 'string' ? opts.cacheDir : undefined,
191-
configFile: typeof opts.configFile === 'string' ? opts.configFile : undefined,
195+
cacheDir: parseOptionalString(opts.cacheDir),
196+
configFile: parseOptionalString(opts.configFile),
192197
showAll: opts.all === true,
193198

194199
// Common export settings
195200
exportCategories: parseCommaSeparated(opts.exportCategories),
196201
exportCountries: parseCommaSeparated(opts.exportCountries),
197202
exportFrom: parseCommaSeparated(opts.exportFrom),
198-
exportStartDate: typeof opts.exportStartDate === 'string' ? opts.exportStartDate : undefined,
199-
exportEndDate: typeof opts.exportEndDate === 'string' ? opts.exportEndDate : undefined,
203+
exportStartDate: parseOptionalString(opts.exportStartDate),
204+
exportEndDate: parseOptionalString(opts.exportEndDate),
200205
exportMinScore: parseOptionalNumber(opts.exportMinScore),
201206
exportOnlyLocations: opts.exportOnlyLocations === true,
202207
exportOnlyGeneric: opts.exportOnlyGeneric === true,
@@ -211,25 +216,25 @@ function buildCLIArgs(commandName: string, input: string, opts: Record<string, u
211216
opts.pdfGroupByCategory,
212217
opts.pdfNoGroupByCategory
213218
),
214-
pdfPageSize: typeof opts.pdfPageSize === 'string' ? opts.pdfPageSize : undefined,
215-
pdfTitle: typeof opts.pdfTitle === 'string' ? opts.pdfTitle : undefined,
216-
pdfSubtitle: typeof opts.pdfSubtitle === 'string' ? opts.pdfSubtitle : undefined,
219+
pdfPageSize: parseOptionalString(opts.pdfPageSize),
220+
pdfTitle: parseOptionalString(opts.pdfTitle),
221+
pdfSubtitle: parseOptionalString(opts.pdfSubtitle),
217222
pdfCategories: parseCommaSeparated(opts.pdfCategories),
218223
pdfCountries: parseCommaSeparated(opts.pdfCountries),
219224
pdfFrom: parseCommaSeparated(opts.pdfFrom),
220-
pdfStartDate: typeof opts.pdfStartDate === 'string' ? opts.pdfStartDate : undefined,
221-
pdfEndDate: typeof opts.pdfEndDate === 'string' ? opts.pdfEndDate : undefined,
225+
pdfStartDate: parseOptionalString(opts.pdfStartDate),
226+
pdfEndDate: parseOptionalString(opts.pdfEndDate),
222227
pdfMinScore: parseOptionalNumber(opts.pdfMinScore),
223228
pdfOnlyLocations: opts.pdfOnlyLocations === true,
224229
pdfOnlyGeneric: opts.pdfOnlyGeneric === true,
225230
pdfMaxActivities: Number.parseInt(String(opts.pdfMaxActivities ?? '0'), 10),
226231
pdfSort: parseSortOrder(opts.pdfSort),
227232

228233
// Map-specific settings
229-
mapDefaultStyle: typeof opts.mapDefaultStyle === 'string' ? opts.mapDefaultStyle : undefined,
234+
mapDefaultStyle: parseOptionalString(opts.mapDefaultStyle),
230235

231236
// Export subcommand settings
232-
exportOutput: typeof opts.output === 'string' ? opts.output : undefined,
237+
exportOutput: parseOptionalString(opts.output),
233238
exportFormat: undefined,
234239

235240
configAction: 'list',

src/cli/commands.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ function addPipelineOptions(cmd: Command): Command {
108108
.option('--min-confidence <num>', 'Minimum confidence threshold', '0.5')
109109
.option('--skip-geocoding', 'Skip geocoding step')
110110
.option('--images', 'Fetch images for activities (slower, uses external APIs)')
111+
.option(
112+
'--media-library-path <path>',
113+
'Local path to media library images (development/offline)'
114+
)
111115
.option('--map-default-style <style>', 'Default map tile style (e.g. osm, satellite, terrain)')
112116
.option('-m, --max-messages <num>', 'Max messages to process (for testing)')
113117
.option('--dry-run', 'Show stats without API calls')
@@ -318,6 +322,10 @@ export function createProgram(): Command {
318322
.option('--skip-pixabay', 'Skip Pixabay image search')
319323
.option('--skip-wikipedia', 'Skip Wikipedia image lookup')
320324
.option('--skip-google-places', 'Skip Google Places photos')
325+
.option(
326+
'--media-library-path <path>',
327+
'Local path to media library images (development/offline)'
328+
)
321329
.option('-n, --max-results <num>', 'Max results to display', '10')
322330
.option('-a, --all', 'Show all activities with images')
323331

src/cli/commands/fetch-image-urls.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ import type { Logger } from '../logger'
1818
import { stepFetchImageUrls } from '../steps/fetch-image-urls'
1919
import { StepRunner } from '../steps/runner'
2020

21-
interface FetchImagesOutput {
21+
interface FetchImagesStats {
2222
activitiesProcessed: number
2323
imagesFound: number
24+
fromMediaLibrary: number
2425
fromCdn: number
2526
fromWikipedia: number
2627
fromPixabay: number
2728
fromGooglePlaces: number
2829
fromUserUpload: number
2930
failed: number
31+
}
32+
33+
interface FetchImagesOutput extends FetchImagesStats {
3034
activities: Array<{
3135
activityId: string
3236
activity: string
@@ -38,6 +42,19 @@ interface FetchImagesOutput {
3842
}>
3943
}
4044

45+
function logStatsSummary(stats: FetchImagesStats, logger: Logger): void {
46+
logger.log('\n📊 Image Fetch Results')
47+
logger.log(` Processed: ${stats.activitiesProcessed}`)
48+
logger.log(` Found: ${stats.imagesFound}`)
49+
if (stats.fromMediaLibrary > 0) logger.log(` From Media Library: ${stats.fromMediaLibrary}`)
50+
if (stats.fromCdn > 0) logger.log(` From CDN: ${stats.fromCdn}`)
51+
if (stats.fromUserUpload > 0) logger.log(` From user uploads: ${stats.fromUserUpload}`)
52+
if (stats.fromWikipedia > 0) logger.log(` From Wikipedia: ${stats.fromWikipedia}`)
53+
if (stats.fromPixabay > 0) logger.log(` From Pixabay: ${stats.fromPixabay}`)
54+
if (stats.fromGooglePlaces > 0) logger.log(` From Google Places: ${stats.fromGooglePlaces}`)
55+
if (stats.failed > 0) logger.log(` Not found: ${stats.failed}`)
56+
}
57+
4158
export async function cmdFetchImageUrls(args: CLIArgs, logger: Logger): Promise<void> {
4259
const { ctx, config } = await initCommandContext('Fetch Image URLs', args, logger)
4360

@@ -68,27 +85,18 @@ export async function cmdFetchImageUrls(args: CLIArgs, logger: Logger): Promise<
6885
}
6986

7087
// Run fetch-image-urls step
88+
// Media library path can come from CLI arg or config
89+
const mediaLibraryPath = args.mediaLibraryPath ?? config?.mediaLibraryPath
7190
const fetchResult = await stepFetchImageUrls(ctx, geocodedActivities, {
7291
skipCdn: args.skipCdn,
7392
skipPixabay: args.skipPixabay,
7493
skipWikipedia: args.skipWikipedia,
75-
skipGooglePlaces: args.skipGooglePlaces
94+
skipGooglePlaces: args.skipGooglePlaces,
95+
mediaLibraryPath
7696
})
7797

7898
// Summary
79-
logger.log('\n📊 Image Fetch Results')
80-
logger.log(` Processed: ${fetchResult.stats.activitiesProcessed}`)
81-
logger.log(` Found: ${fetchResult.stats.imagesFound}`)
82-
if (fetchResult.stats.fromCdn > 0) logger.log(` From CDN: ${fetchResult.stats.fromCdn}`)
83-
if (fetchResult.stats.fromUserUpload > 0)
84-
logger.log(` From user uploads: ${fetchResult.stats.fromUserUpload}`)
85-
if (fetchResult.stats.fromWikipedia > 0)
86-
logger.log(` From Wikipedia: ${fetchResult.stats.fromWikipedia}`)
87-
if (fetchResult.stats.fromPixabay > 0)
88-
logger.log(` From Pixabay: ${fetchResult.stats.fromPixabay}`)
89-
if (fetchResult.stats.fromGooglePlaces > 0)
90-
logger.log(` From Google Places: ${fetchResult.stats.fromGooglePlaces}`)
91-
if (fetchResult.stats.failed > 0) logger.log(` Not found: ${fetchResult.stats.failed}`)
99+
logStatsSummary(fetchResult.stats, logger)
92100

93101
const output: FetchImagesOutput = {
94102
...fetchResult.stats,

src/cli/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface Config {
2626
outputDir?: string | undefined
2727
/** Export formats (csv,excel,json,map,pdf) */
2828
formats?: string[] | undefined
29+
/** Local path to media library images (for development/offline use) */
30+
mediaLibraryPath?: string | undefined
2931

3032
// === Common export settings (apply to ALL formats) ===
3133
/** Filter ALL exports by categories */
@@ -102,6 +104,7 @@ const STRING_KEYS: ConfigKey[] = [
102104
'timezone',
103105
'cacheDir',
104106
'outputDir',
107+
'mediaLibraryPath',
105108
// Common export
106109
'exportStartDate',
107110
'exportEndDate',
@@ -159,6 +162,7 @@ const CONFIG_DESCRIPTIONS: Record<ConfigKey, string> = {
159162
fetchImages: 'Fetch images by default (default: false)',
160163
formats: 'Export formats (default: csv,excel,json,map,pdf)',
161164
homeCountry: 'Your home country for location context (default: detected from IP)',
165+
mediaLibraryPath: 'Local path to media library images (for development/offline use)',
162166
outputDir: 'Output directory for exports (default: ./output)',
163167
timezone: 'Your timezone (e.g. Pacific/Auckland) (default: detected from system)',
164168

src/cli/steps/fetch-image-urls.ts

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const DEFAULT_CONCURRENCY = 10
2525
interface FetchImagesStats {
2626
readonly activitiesProcessed: number
2727
readonly imagesFound: number
28+
readonly fromMediaLibrary: number
2829
readonly fromCdn: number
2930
readonly fromGooglePlaces: number
3031
readonly fromWikipedia: number
@@ -33,6 +34,19 @@ interface FetchImagesStats {
3334
readonly failed: number
3435
}
3536

37+
/** Mutable version for accumulation */
38+
interface MutableStats {
39+
activitiesProcessed: number
40+
imagesFound: number
41+
fromMediaLibrary: number
42+
fromCdn: number
43+
fromGooglePlaces: number
44+
fromWikipedia: number
45+
fromPixabay: number
46+
fromUserUpload: number
47+
failed: number
48+
}
49+
3650
/**
3751
* Result of the fetch images step.
3852
*/
@@ -63,37 +77,23 @@ interface FetchImagesOptions {
6377
readonly googlePlacesApiKey?: string | undefined
6478
/** Number of concurrent image fetches (default 10) */
6579
readonly concurrency?: number | undefined
80+
/** Local path to media library images (for development/offline use) */
81+
readonly mediaLibraryPath?: string | undefined
6682
}
6783

6884
/** Serializable version of image results for caching */
6985
interface CachedImageData {
7086
readonly entries: Array<[string, ImageResult | null]>
7187
}
7288

73-
/**
74-
* Count source from image result.
75-
*/
76-
function countSource(result: ImageResult): keyof Omit<FetchImagesStats, 'activitiesProcessed'> {
77-
const sourceMap: Record<
78-
ImageResult['source'],
79-
keyof Omit<FetchImagesStats, 'activitiesProcessed'>
80-
> = {
81-
cdn: 'fromCdn',
82-
google_places: 'fromGooglePlaces',
83-
wikipedia: 'fromWikipedia',
84-
pixabay: 'fromPixabay',
85-
user_upload: 'fromUserUpload'
86-
}
87-
return sourceMap[result.source]
88-
}
89-
9089
/**
9190
* Calculate stats from image results.
9291
*/
9392
function calculateStats(images: Map<string, ImageResult | null>): FetchImagesStats {
94-
const stats = {
93+
const stats: MutableStats = {
9594
activitiesProcessed: images.size,
9695
imagesFound: 0,
96+
fromMediaLibrary: 0,
9797
fromCdn: 0,
9898
fromGooglePlaces: 0,
9999
fromWikipedia: 0,
@@ -105,7 +105,7 @@ function calculateStats(images: Map<string, ImageResult | null>): FetchImagesSta
105105
for (const result of images.values()) {
106106
if (result) {
107107
stats.imagesFound++
108-
stats[countSource(result)]++
108+
incrementSourceCount(stats, result.source)
109109
} else {
110110
stats.failed++
111111
}
@@ -114,11 +114,38 @@ function calculateStats(images: Map<string, ImageResult | null>): FetchImagesSta
114114
return stats
115115
}
116116

117+
/**
118+
* Increment the appropriate source count based on image source.
119+
*/
120+
function incrementSourceCount(stats: MutableStats, source: ImageResult['source']): void {
121+
switch (source) {
122+
case 'media_library':
123+
stats.fromMediaLibrary++
124+
break
125+
case 'cdn':
126+
stats.fromCdn++
127+
break
128+
case 'google_places':
129+
stats.fromGooglePlaces++
130+
break
131+
case 'wikipedia':
132+
stats.fromWikipedia++
133+
break
134+
case 'pixabay':
135+
stats.fromPixabay++
136+
break
137+
case 'user_upload':
138+
stats.fromUserUpload++
139+
break
140+
}
141+
}
142+
117143
/**
118144
* Log stats about fetched images.
119145
*/
120146
function logStats(stats: FetchImagesStats, logger: PipelineContext['logger']): void {
121147
logger.log(` ✓ ${stats.imagesFound}/${stats.activitiesProcessed} images found`)
148+
if (stats.fromMediaLibrary > 0) logger.log(` 📸 ${stats.fromMediaLibrary} from Media Library`)
122149
if (stats.fromCdn > 0) logger.log(` 📦 ${stats.fromCdn} from CDN`)
123150
if (stats.fromUserUpload > 0) logger.log(` 📤 ${stats.fromUserUpload} from user uploads`)
124151
if (stats.fromWikipedia > 0) logger.log(` 📚 ${stats.fromWikipedia} from Wikipedia`)
@@ -160,6 +187,7 @@ export async function stepFetchImageUrls(
160187
const stats: FetchImagesStats = {
161188
activitiesProcessed: 0,
162189
imagesFound: 0,
190+
fromMediaLibrary: 0,
163191
fromCdn: 0,
164192
fromGooglePlaces: 0,
165193
fromWikipedia: 0,
@@ -185,7 +213,8 @@ export async function stepFetchImageUrls(
185213
googlePlacesApiKey:
186214
options?.googlePlacesApiKey ??
187215
process.env.GOOGLE_MAPS_API_KEY ??
188-
process.env.GOOGLE_MAPS_API_KEY
216+
process.env.GOOGLE_MAPS_API_KEY,
217+
mediaLibraryPath: options?.mediaLibraryPath
189218
}
190219

191220
// Log what sources are available

src/cli/steps/runner.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,16 @@ export class StepRunner {
192192
// Dependency: geocode
193193
const { activities } = await this.run('geocode')
194194

195+
// Media library path can come from CLI arg or config
196+
const mediaLibraryPath = this.args.mediaLibraryPath ?? this.config?.mediaLibraryPath
197+
195198
const { stepFetchImageUrls } = await import('./fetch-image-urls')
196199
const result = await stepFetchImageUrls(this.ctx, activities, {
197200
skipCdn: this.args.skipCdn,
198201
skipPixabay: this.args.skipPixabay,
199202
skipWikipedia: this.args.skipWikipedia,
200-
skipGooglePlaces: this.args.skipGooglePlaces
203+
skipGooglePlaces: this.args.skipGooglePlaces,
204+
mediaLibraryPath
201205
})
202206
return { images: result.images }
203207
}

0 commit comments

Comments
 (0)