@yuiseki/gyazocli 0.0.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,2268 +6,98 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const commander_1 = require("commander");
8
8
  const fs_1 = __importDefault(require("fs"));
9
- const path_1 = __importDefault(require("path"));
10
- const api_1 = require("./api");
11
- const storage_1 = require("./storage");
12
- const credentials_1 = require("./credentials");
9
+ const ids_1 = require("./ids");
10
+ const config_1 = require("./commands/config");
11
+ const list_1 = require("./commands/list");
12
+ const get_1 = require("./commands/get");
13
+ const collection_1 = require("./commands/collection");
14
+ const search_1 = require("./commands/search");
15
+ const apps_1 = require("./commands/apps");
16
+ const domains_1 = require("./commands/domains");
17
+ const tags_1 = require("./commands/tags");
18
+ const locations_1 = require("./commands/locations");
19
+ const summary_1 = require("./commands/summary");
20
+ const stats_1 = require("./commands/stats");
21
+ const upload_1 = require("./commands/upload");
22
+ const sync_1 = require("./commands/sync");
23
+ const import_1 = require("./commands/import");
13
24
  const program = new commander_1.Command();
14
- const UPLOAD_DESC_TAG = '#gyazocli_uploads';
15
- const WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
16
25
  program
17
26
  .name('gyazo')
18
27
  .description('Gyazo Memory CLI for AI Secretary')
19
- .version('0.0.2');
20
- // Config Command
21
- const configCmd = program.command('config').description('Manage configuration');
22
- configCmd
23
- .command('set <key> <value>')
24
- .description('Set a configuration value')
25
- .action((key, value) => {
26
- (0, credentials_1.setStoredConfig)(key, value);
27
- });
28
- configCmd
29
- .command('get <key>')
30
- .description('Get a configuration value')
31
- .option('-j, --json', 'output as JSON')
32
- .action(async (key, options) => {
33
- if (key === 'me') {
34
- await (0, credentials_1.ensureAccessToken)();
35
- try {
36
- const me = await (0, api_1.getCurrentUser)();
37
- if (options.json) {
38
- console.log(JSON.stringify(me, null, 2));
39
- return;
40
- }
41
- const user = me?.user || {};
42
- if (user.uid)
43
- console.log(`UID: ${user.uid}`);
44
- if (user.name)
45
- console.log(`Name: ${user.name}`);
46
- if (user.email)
47
- console.log(`Email: ${user.email}`);
48
- if (typeof user.is_pro === 'boolean')
49
- console.log(`Plan: ${user.is_pro ? 'Pro' : 'Free'}`);
50
- if (typeof user.is_team === 'boolean')
51
- console.log(`Team: ${user.is_team ? 'Yes' : 'No'}`);
52
- if (user.profile_image)
53
- console.log(`Profile image: ${user.profile_image}`);
54
- }
55
- catch (error) {
56
- console.error('Error getting current user:', error.message);
57
- process.exit(1);
58
- }
59
- return;
60
- }
61
- const value = (0, credentials_1.getStoredConfig)(key);
62
- if (value) {
63
- if (key === 'token') {
64
- const masked = value.length > 8
65
- ? `${value.substring(0, 4)}...${value.substring(value.length - 4)}`
66
- : '********';
67
- console.log(masked);
68
- }
69
- else {
70
- console.log(value);
71
- }
72
- }
73
- else {
74
- console.error(`Config key '${key}' not found.`);
28
+ .option('--mcp-server', 'run as a Model Context Protocol server over stdio')
29
+ .version('0.2.0');
30
+ (0, config_1.registerConfigCommand)(program);
31
+ (0, list_1.registerListCommand)(program);
32
+ (0, get_1.registerGetCommand)(program);
33
+ (0, collection_1.registerCollectionCommand)(program);
34
+ (0, search_1.registerSearchCommand)(program);
35
+ (0, apps_1.registerAppsCommand)(program);
36
+ (0, domains_1.registerDomainsCommand)(program);
37
+ (0, tags_1.registerTagsCommand)(program);
38
+ (0, locations_1.registerLocationsCommand)(program);
39
+ (0, summary_1.registerSummaryCommand)(program);
40
+ (0, stats_1.registerStatsCommand)(program);
41
+ (0, upload_1.registerUploadCommand)(program);
42
+ (0, sync_1.registerSyncCommand)(program);
43
+ (0, import_1.registerImportCommand)(program);
44
+ /**
45
+ * Let the first argument stand on its own when it is unambiguous:
46
+ * a Gyazo image ID or URL means `get`, an existing file means `upload`.
47
+ * Anything else is left to commander so unknown commands still report as such.
48
+ */
49
+ function expandImplicitCommand(argv) {
50
+ const args = argv.slice(2);
51
+ const first = args[0];
52
+ if (!first || first.startsWith('-')) {
53
+ return argv;
54
+ }
55
+ const knownNames = new Set(['help']);
56
+ for (const command of program.commands) {
57
+ knownNames.add(command.name());
58
+ for (const alias of command.aliases()) {
59
+ knownNames.add(alias);
60
+ }
61
+ }
62
+ if (knownNames.has(first)) {
63
+ return argv;
64
+ }
65
+ let implicitCommand = null;
66
+ if ((0, ids_1.normalizeImageId)(first)) {
67
+ // A bare 32-hex ID is ambiguous; treat it as an image.
68
+ implicitCommand = 'get';
69
+ }
70
+ else if ((0, ids_1.normalizeCollectionId)(first)) {
71
+ // Only the /collections/<id> URL form is unambiguous.
72
+ implicitCommand = 'collection';
73
+ }
74
+ else if (fs_1.default.existsSync(first) && fs_1.default.statSync(first).isFile()) {
75
+ implicitCommand = 'upload';
76
+ }
77
+ if (!implicitCommand) {
78
+ return argv;
79
+ }
80
+ return [...argv.slice(0, 2), implicitCommand, ...args];
81
+ }
82
+ /**
83
+ * The MCP server is not a commander command: it owns stdout for the whole
84
+ * process, so it is dispatched before parsing rather than from an action.
85
+ * The spellings a client is likely to be configured with all work.
86
+ */
87
+ const MCP_INVOCATIONS = new Set(['--mcp-server', '--mcp', 'mcp-server', 'mcp']);
88
+ function isMcpInvocation(argv) {
89
+ const first = argv.slice(2)[0];
90
+ return first !== undefined && MCP_INVOCATIONS.has(first);
91
+ }
92
+ if (isMcpInvocation(process.argv)) {
93
+ // Required lazily: the MCP SDK is a large import that every other command
94
+ // would otherwise pay for at startup.
95
+ const { runMcpServer } = require('./mcp');
96
+ runMcpServer().catch((error) => {
97
+ console.error('MCP server failed:', error?.message || error);
75
98
  process.exit(1);
76
- }
77
- });
78
- function isToday(date) {
79
- const today = new Date();
80
- return date.getDate() === today.getDate() &&
81
- date.getMonth() === today.getMonth() &&
82
- date.getFullYear() === today.getFullYear();
83
- }
84
- function normalizeText(value) {
85
- if (!value)
86
- return undefined;
87
- const normalized = value.replace(/\s+/g, ' ').trim();
88
- return normalized.length > 0 ? normalized : undefined;
89
- }
90
- function extractDomain(value) {
91
- if (!value)
92
- return undefined;
93
- try {
94
- const url = new URL(value);
95
- return url.hostname.replace(/^www\./, '');
96
- }
97
- catch (e) {
98
- try {
99
- const url = new URL(`https://${value}`);
100
- return url.hostname.replace(/^www\./, '');
101
- }
102
- catch (_e) {
103
- return undefined;
104
- }
105
- }
106
- }
107
- function isXDomain(domain) {
108
- if (!domain)
109
- return false;
110
- return domain === 'x.com' ||
111
- domain.endsWith('.x.com') ||
112
- domain === 'twitter.com' ||
113
- domain.endsWith('.twitter.com');
114
- }
115
- function cleanTextForDomain(value, domain) {
116
- if (!isXDomain(domain))
117
- return value;
118
- return value
119
- .replace(/^Xユーザーの/, '')
120
- .replace(/\s*\/\s*X$/, '')
121
- .trim();
122
- }
123
- function stripInlineUrls(value) {
124
- return value
125
- .replace(/https?:\/\/\S+/g, '')
126
- .replace(/\bwww\.\S+/g, '')
127
- .replace(/\s+/g, ' ')
128
- .trim();
129
- }
130
- function sanitizeSummaryText(value, domain) {
131
- if (!value)
132
- return undefined;
133
- return normalizeText(stripInlineUrls(cleanTextForDomain(value, domain)));
134
- }
135
- function getAddressEntry(exifAddress, locale) {
136
- if (!exifAddress || typeof exifAddress !== 'object')
137
- return undefined;
138
- if (typeof exifAddress.address === 'string')
139
- return exifAddress;
140
- const entry = exifAddress[locale];
141
- if (!entry || typeof entry !== 'object')
142
- return undefined;
143
- return entry;
144
- }
145
- function getAddressComponent(addressEntry, type) {
146
- if (!addressEntry || typeof addressEntry !== 'object')
147
- return undefined;
148
- const components = Array.isArray(addressEntry.address_components)
149
- ? addressEntry.address_components
150
- : [];
151
- for (const component of components) {
152
- if (!component || typeof component !== 'object')
153
- continue;
154
- const types = Array.isArray(component.types) ? component.types : [];
155
- if (!types.includes(type))
156
- continue;
157
- const value = normalizeText(component.long_name || component.short_name);
158
- if (value)
159
- return value;
160
- }
161
- return undefined;
162
- }
163
- function buildJaLocationLabel(exifAddress) {
164
- const ja = getAddressEntry(exifAddress, 'ja');
165
- if (!ja)
166
- return undefined;
167
- const pref = getAddressComponent(ja, 'administrative_area_level_1');
168
- const locality = getAddressComponent(ja, 'locality') || getAddressComponent(ja, 'administrative_area_level_2');
169
- const sublocality = getAddressComponent(ja, 'sublocality_level_2') ||
170
- getAddressComponent(ja, 'sublocality_level_1') ||
171
- getAddressComponent(ja, 'sublocality_level_3');
172
- const fromComponents = normalizeText([pref, locality, sublocality].filter(Boolean).join(''));
173
- if (fromComponents)
174
- return fromComponents;
175
- const raw = normalizeText(ja.address);
176
- if (!raw)
177
- return undefined;
178
- const compact = raw
179
- .replace(/^日本、?/, '')
180
- .replace(/〒\d{3}-\d{4}\s*/g, '')
181
- .replace(/[0-90-9].*$/, '')
182
- .trim();
183
- return normalizeText(compact);
184
- }
185
- function buildEnLocationLabel(exifAddress) {
186
- const en = getAddressEntry(exifAddress, 'en');
187
- if (!en)
188
- return undefined;
189
- const pref = getAddressComponent(en, 'administrative_area_level_1');
190
- const locality = getAddressComponent(en, 'locality') || getAddressComponent(en, 'administrative_area_level_2');
191
- const sublocality = getAddressComponent(en, 'sublocality_level_2') ||
192
- getAddressComponent(en, 'sublocality_level_1') ||
193
- getAddressComponent(en, 'sublocality_level_3');
194
- const fromComponents = normalizeText([sublocality, locality, pref].filter(Boolean).join(', '));
195
- if (fromComponents)
196
- return fromComponents;
197
- return normalizeText(en.address);
198
- }
199
- function extractImageAddressText(img) {
200
- const exifAddress = img.metadata?.exif_address ?? img.exif_address;
201
- if (!exifAddress)
202
- return undefined;
203
- if (typeof exifAddress === 'string')
204
- return normalizeText(exifAddress);
205
- if (typeof exifAddress !== 'object')
206
- return undefined;
207
- const ja = getAddressEntry(exifAddress, 'ja');
208
- const jaAddress = normalizeText(ja?.address);
209
- if (jaAddress)
210
- return jaAddress;
211
- const en = getAddressEntry(exifAddress, 'en');
212
- const enAddress = normalizeText(en?.address);
213
- if (enAddress)
214
- return enAddress;
215
- for (const value of Object.values(exifAddress)) {
216
- if (!value || typeof value !== 'object')
217
- continue;
218
- const raw = normalizeText(value.address);
219
- if (raw)
220
- return raw;
221
- }
222
- return undefined;
223
- }
224
- function extractImageLocationLabel(img) {
225
- const exifAddress = img.metadata?.exif_address ?? img.exif_address;
226
- if (!exifAddress)
227
- return undefined;
228
- if (typeof exifAddress === 'string')
229
- return normalizeText(exifAddress);
230
- if (typeof exifAddress !== 'object')
231
- return undefined;
232
- const jaLabel = buildJaLocationLabel(exifAddress);
233
- if (jaLabel)
234
- return jaLabel;
235
- const enLabel = buildEnLocationLabel(exifAddress);
236
- if (enLabel)
237
- return enLabel;
238
- for (const value of Object.values(exifAddress)) {
239
- if (!value || typeof value !== 'object')
240
- continue;
241
- const raw = normalizeText(value.address);
242
- if (raw)
243
- return raw;
244
- }
245
- return undefined;
246
- }
247
- function truncateText(value, maxLength) {
248
- if (value.length <= maxLength)
249
- return value;
250
- if (maxLength <= 3)
251
- return value.slice(0, maxLength);
252
- return `${value.slice(0, maxLength - 3)}...`;
253
- }
254
- function formatCreatedAt(value) {
255
- const match = value.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}):(\d{2})/);
256
- if (match) {
257
- return `${match[1]} ${match[2]}:${match[3]}`;
258
- }
259
- return value;
260
- }
261
- function shortenImageId(imageId) {
262
- if (!imageId)
263
- return '';
264
- if (imageId.length <= 4)
265
- return imageId;
266
- return `${imageId.slice(0, 4)}...`;
267
- }
268
- function formatTerminalLink(label, url) {
269
- if (!url || !process.stdout.isTTY)
270
- return label;
271
- return `\u001B]8;;${url}\u0007${label}\u001B]8;;\u0007`;
272
- }
273
- function normalizeOcrText(value) {
274
- if (!value)
275
- return undefined;
276
- const normalized = value
277
- .replace(/\r\n/g, '\n')
278
- .replace(/\r/g, '\n')
279
- .split('\n')
280
- .map(line => line.trimEnd())
281
- .join('\n')
282
- .trim();
283
- return normalized.length > 0 ? normalized : undefined;
284
- }
285
- function extractOcrDescription(image) {
286
- const direct = normalizeOcrText(image?.ocr?.description);
287
- if (direct)
288
- return direct;
289
- return normalizeOcrText(image?.metadata?.ocr?.description);
290
- }
291
- function buildOcrPreview(ocrText, maxLines) {
292
- const lines = ocrText.split('\n');
293
- if (lines.length <= maxLines) {
294
- return { text: ocrText, truncated: false };
295
- }
296
- return {
297
- text: lines.slice(0, maxLines).join('\n'),
298
- truncated: true,
299
- };
300
- }
301
- function extractObjectAnnotations(image) {
302
- const rawAnnotations = image?.localizedObjectAnnotations ||
303
- image?.localized_object_annotations ||
304
- image?.metadata?.localizedObjectAnnotations ||
305
- image?.metadata?.localized_object_annotations ||
306
- [];
307
- if (!Array.isArray(rawAnnotations))
308
- return [];
309
- const bestByName = new Map();
310
- for (const annotation of rawAnnotations) {
311
- if (!annotation || typeof annotation !== 'object')
312
- continue;
313
- const name = normalizeText(annotation.name_ja || annotation.nameJa || annotation.name);
314
- if (!name)
315
- continue;
316
- const score = typeof annotation.score === 'number' ? annotation.score : undefined;
317
- const existing = bestByName.get(name);
318
- if (!existing) {
319
- bestByName.set(name, { name, score });
320
- continue;
321
- }
322
- const existingScore = existing.score ?? -1;
323
- const nextScore = score ?? -1;
324
- if (nextScore > existingScore) {
325
- bestByName.set(name, { name, score });
326
- }
327
- }
328
- return Array.from(bestByName.values()).sort((a, b) => {
329
- const sa = a.score ?? -1;
330
- const sb = b.score ?? -1;
331
- return sb - sa;
332
- });
333
- }
334
- function formatObjectAnnotationLine(annotation) {
335
- if (typeof annotation.score === 'number') {
336
- return `${annotation.name} (${(annotation.score * 100).toFixed(1)}%)`;
337
- }
338
- return annotation.name;
339
- }
340
- function ensureUploadDescTag(desc) {
341
- const normalized = normalizeText(desc);
342
- if (!normalized)
343
- return UPLOAD_DESC_TAG;
344
- const words = normalized
345
- .split(' ')
346
- .filter(word => word.toLowerCase() !== UPLOAD_DESC_TAG.toLowerCase());
347
- words.push(UPLOAD_DESC_TAG);
348
- return words.join(' ').trim();
349
- }
350
- function parseUploadTimestamp(value) {
351
- if (!value)
352
- return undefined;
353
- if (!/^\d+$/.test(value)) {
354
- console.error('Error: --timestamp must be a unix timestamp in seconds.');
355
- process.exit(1);
356
- }
357
- const parsed = Number(value);
358
- if (!Number.isSafeInteger(parsed)) {
359
- console.error('Error: --timestamp is out of range.');
360
- process.exit(1);
361
- }
362
- const now = Math.floor(Date.now() / 1000);
363
- if (parsed > now) {
364
- console.error('Error: --timestamp must be current time or in the past.');
365
- process.exit(1);
366
- }
367
- return parsed;
368
- }
369
- function parsePositiveIntegerOption(value, optionName) {
370
- if (!/^\d+$/.test(value)) {
371
- console.error(`Error: ${optionName} must be a positive integer.`);
372
- process.exit(1);
373
- }
374
- const parsed = Number(value);
375
- if (!Number.isSafeInteger(parsed) || parsed <= 0) {
376
- console.error(`Error: ${optionName} must be a positive integer.`);
377
- process.exit(1);
378
- }
379
- return parsed;
380
- }
381
- function parseDateOption(value) {
382
- if (!value) {
383
- const today = new Date();
384
- const year = String(today.getFullYear());
385
- const month = String(today.getMonth() + 1).padStart(2, '0');
386
- const day = String(today.getDate()).padStart(2, '0');
387
- return {
388
- granularity: 'day',
389
- dateKey: `${year}-${month}-${day}`,
390
- start: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0),
391
- end: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59, 999),
392
- };
393
- }
394
- if (/^\d{4}$/.test(value)) {
395
- const year = Number(value);
396
- return {
397
- granularity: 'year',
398
- dateKey: value,
399
- start: new Date(year, 0, 1, 0, 0, 0, 0),
400
- end: new Date(year, 11, 31, 23, 59, 59, 999),
401
- };
402
- }
403
- if (/^\d{4}-\d{2}$/.test(value)) {
404
- const [yearText, monthText] = value.split('-');
405
- const year = Number(yearText);
406
- const month = Number(monthText);
407
- const probe = new Date(year, month - 1, 1);
408
- if (probe.getFullYear() !== year || probe.getMonth() !== month - 1) {
409
- console.error('Error: --date month is invalid.');
410
- process.exit(1);
411
- }
412
- return {
413
- granularity: 'month',
414
- dateKey: value,
415
- start: new Date(year, month - 1, 1, 0, 0, 0, 0),
416
- end: new Date(year, month, 0, 23, 59, 59, 999),
417
- };
418
- }
419
- if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
420
- const [yearText, monthText, dayText] = value.split('-');
421
- const year = Number(yearText);
422
- const month = Number(monthText);
423
- const day = Number(dayText);
424
- const probe = new Date(year, month - 1, day);
425
- if (probe.getFullYear() !== year ||
426
- probe.getMonth() !== month - 1 ||
427
- probe.getDate() !== day) {
428
- console.error('Error: --date day is invalid.');
429
- process.exit(1);
430
- }
431
- return {
432
- granularity: 'day',
433
- dateKey: value,
434
- start: new Date(year, month - 1, day, 0, 0, 0, 0),
435
- end: new Date(year, month - 1, day, 23, 59, 59, 999),
436
- };
437
- }
438
- console.error('Error: --date format must be yyyy or yyyy-mm or yyyy-mm-dd.');
439
- process.exit(1);
440
- }
441
- function formatDateYmd(date) {
442
- const year = String(date.getFullYear());
443
- const month = String(date.getMonth() + 1).padStart(2, '0');
444
- const day = String(date.getDate()).padStart(2, '0');
445
- return `${year}-${month}-${day}`;
446
- }
447
- function buildRecentWeekRangeUntilYesterday() {
448
- const today = new Date();
449
- const end = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1, 23, 59, 59, 999);
450
- const start = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 8, 0, 0, 0, 0);
451
- return {
452
- granularity: 'day',
453
- dateKey: `${formatDateYmd(start)}..${formatDateYmd(end)}`,
454
- start,
455
- end,
456
- };
457
- }
458
- function resolveRankingRangeOption(options) {
459
- if (options.today && options.date) {
460
- console.error('Error: --today and --date cannot be used together.');
461
- process.exit(1);
462
- }
463
- if (options.today) {
464
- return parseDateOption();
465
- }
466
- if (options.date) {
467
- return parseDateOption(options.date);
468
- }
469
- return buildRecentWeekRangeUntilYesterday();
470
- }
471
- function buildStatsDateRange(dateOption, daysOption) {
472
- if (!dateOption && daysOption === '7') {
473
- const weekly = buildRecentWeekRangeUntilYesterday();
474
- return {
475
- range: weekly,
476
- days: 7,
477
- startLabel: formatDateYmd(weekly.start),
478
- endLabel: formatDateYmd(weekly.end),
479
- };
480
- }
481
- const days = parsePositiveIntegerOption(daysOption, '--days');
482
- let endDate;
483
- if (dateOption) {
484
- const parsed = parseDateOption(dateOption);
485
- endDate = new Date(parsed.end);
486
- }
487
- else {
488
- const now = new Date();
489
- endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 23, 59, 59, 999);
490
- }
491
- const startDate = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate(), 0, 0, 0, 0);
492
- startDate.setDate(startDate.getDate() - (days - 1));
493
- const startLabel = formatDateYmd(startDate);
494
- const endLabel = formatDateYmd(endDate);
495
- return {
496
- range: {
497
- granularity: 'day',
498
- dateKey: `${startLabel}..${endLabel}`,
499
- start: startDate,
500
- end: endDate,
501
- },
502
- days,
503
- startLabel,
504
- endLabel,
505
- };
506
- }
507
- function getDateHourStrings() {
508
- const hours = [];
509
- for (let hour = 0; hour < 24; hour++) {
510
- hours.push(String(hour).padStart(2, '0'));
511
- }
512
- return hours;
513
- }
514
- function buildHourlyBucketKey(year, month, day, hour) {
515
- return `${year}-${month}-${day}-${hour}`;
516
- }
517
- function splitHourlyBucketKey(key) {
518
- const [year, month, day, hour] = key.split('-');
519
- return { year, month, day, hour };
520
- }
521
- function toDateParts(date) {
522
- return {
523
- year: String(date.getFullYear()),
524
- month: String(date.getMonth() + 1).padStart(2, '0'),
525
- day: String(date.getDate()).padStart(2, '0'),
526
- hour: String(date.getHours()).padStart(2, '0'),
527
- };
528
- }
529
- function getDatePartsInRange(start, end) {
530
- const dates = [];
531
- const cursor = new Date(start.getFullYear(), start.getMonth(), start.getDate(), 0, 0, 0, 0);
532
- const last = new Date(end.getFullYear(), end.getMonth(), end.getDate(), 0, 0, 0, 0);
533
- while (cursor.getTime() <= last.getTime()) {
534
- dates.push({
535
- year: String(cursor.getFullYear()),
536
- month: String(cursor.getMonth() + 1).padStart(2, '0'),
537
- day: String(cursor.getDate()).padStart(2, '0'),
538
- });
539
- cursor.setDate(cursor.getDate() + 1);
540
- }
541
- return dates;
542
- }
543
- function loadImageIdsFromDateRangeCache(targetDate) {
544
- const imageIds = new Set();
545
- const dates = getDatePartsInRange(targetDate.start, targetDate.end);
546
- const hours = getDateHourStrings();
547
- for (const date of dates) {
548
- for (const hour of hours) {
549
- const ids = (0, storage_1.loadHourlyCache)(date.year, date.month, date.day, hour) || [];
550
- for (const id of ids)
551
- imageIds.add(id);
552
- }
553
- }
554
- return Array.from(imageIds);
555
- }
556
- function normalizeRankingValues(values) {
557
- const uniqueByLower = new Map();
558
- for (const raw of values) {
559
- const value = normalizeText(raw);
560
- if (!value)
561
- continue;
562
- const key = value.toLocaleLowerCase();
563
- if (!uniqueByLower.has(key)) {
564
- uniqueByLower.set(key, value);
565
- }
566
- }
567
- return Array.from(uniqueByLower.values());
568
- }
569
- function normalizeHourlyMetadataEntries(valuesByImageId) {
570
- if (!valuesByImageId || typeof valuesByImageId !== 'object')
571
- return {};
572
- const normalized = {};
573
- for (const [imageId, rawValues] of Object.entries(valuesByImageId)) {
574
- const values = Array.isArray(rawValues)
575
- ? rawValues.map(value => String(value))
576
- : [];
577
- normalized[imageId] = normalizeRankingValues(values);
578
- }
579
- return normalized;
580
- }
581
- function extractImageApps(image) {
582
- const app = normalizeText(image?.metadata?.app);
583
- return app ? [app] : [];
584
- }
585
- function extractImageDomains(image) {
586
- const domain = extractDomain(normalizeText(image?.metadata?.url));
587
- return domain ? [domain] : [];
588
- }
589
- function extractImageLocations(image) {
590
- const location = normalizeText(extractImageLocationLabel(image));
591
- return location ? [location] : [];
592
- }
593
- function normalizeTagText(value) {
594
- if (!value)
595
- return undefined;
596
- const normalized = normalizeText(value);
597
- if (!normalized)
598
- return undefined;
599
- const stripped = normalized.replace(/^[##]+/, '').trim();
600
- return stripped.length > 0 ? stripped : undefined;
601
- }
602
- function extractTagFromLinkValue(value) {
603
- if (typeof value === 'string') {
604
- return normalizeTagText(value);
605
- }
606
- if (!value || typeof value !== 'object')
607
- return undefined;
608
- const candidates = [
609
- value.tag,
610
- value.name,
611
- value.title,
612
- value.text,
613
- value.keyword,
614
- ];
615
- for (const candidate of candidates) {
616
- const tag = normalizeTagText(candidate);
617
- if (tag)
618
- return tag;
619
- }
620
- return undefined;
621
- }
622
- function extractImageTags(image) {
623
- const rawLinks = image?.metadata?.links ?? image?.links;
624
- if (!Array.isArray(rawLinks))
625
- return [];
626
- const tags = [];
627
- for (const rawLink of rawLinks) {
628
- const tag = extractTagFromLinkValue(rawLink);
629
- if (tag)
630
- tags.push(tag);
631
- }
632
- return normalizeRankingValues(tags);
633
- }
634
- async function warmDateCacheForApps(targetDate, maxPages, useCache) {
635
- return warmDateCacheForRanking(targetDate, maxPages, useCache, 'apps', extractImageApps);
636
- }
637
- async function warmDateCacheForDomains(targetDate, maxPages, useCache) {
638
- return warmDateCacheForRanking(targetDate, maxPages, useCache, 'domains', extractImageDomains);
639
- }
640
- async function warmDateCacheForTags(targetDate, maxPages, useCache) {
641
- return warmDateCacheForRanking(targetDate, maxPages, useCache, 'tags', extractImageTags);
642
- }
643
- async function warmDateCacheForLocations(targetDate, maxPages, useCache) {
644
- return warmDateCacheForRanking(targetDate, maxPages, useCache, 'locations', extractImageLocations);
645
- }
646
- async function warmDateCacheForList(targetDate, maxPages, useCache) {
647
- const hourlyIndices = new Map();
648
- const imageIds = new Set();
649
- for (let page = 1; page <= maxPages; page++) {
650
- const images = await (0, api_1.listImages)(page, 100);
651
- if (images.length === 0)
652
- break;
653
- let reachedLimit = false;
654
- for (const img of images) {
655
- const createdAt = new Date(img.created_at);
656
- if (Number.isNaN(createdAt.getTime()))
657
- continue;
658
- if (createdAt > targetDate.end)
659
- continue;
660
- if (createdAt < targetDate.start) {
661
- reachedLimit = true;
662
- break;
663
- }
664
- const dateParts = toDateParts(createdAt);
665
- const bucketKey = buildHourlyBucketKey(dateParts.year, dateParts.month, dateParts.day, dateParts.hour);
666
- if (!hourlyIndices.has(bucketKey)) {
667
- hourlyIndices.set(bucketKey, new Set());
668
- }
669
- hourlyIndices.get(bucketKey)?.add(img.image_id);
670
- imageIds.add(img.image_id);
671
- let merged = img;
672
- const cached = useCache ? (0, storage_1.loadImageCache)(img.image_id) : null;
673
- if (cached) {
674
- merged = mergeImageForDisplay(img, cached);
675
- }
676
- (0, storage_1.saveImageCache)(img.image_id, merged);
677
- }
678
- if (reachedLimit)
679
- break;
680
- }
681
- for (const [bucketKey, current] of hourlyIndices.entries()) {
682
- const { year, month, day, hour } = splitHourlyBucketKey(bucketKey);
683
- if (useCache) {
684
- const existing = (0, storage_1.loadHourlyCache)(year, month, day, hour) || [];
685
- for (const id of existing)
686
- current.add(id);
687
- }
688
- (0, storage_1.saveHourlyCache)(year, month, day, hour, Array.from(current));
689
- for (const id of current)
690
- imageIds.add(id);
691
- }
692
- if (useCache) {
693
- for (const id of loadImageIdsFromDateRangeCache(targetDate)) {
694
- imageIds.add(id);
695
- }
696
- }
697
- return Array.from(imageIds);
698
- }
699
- async function warmDateCacheForRanking(targetDate, maxPages, useCache, metadataKind, extractValues) {
700
- const hourlyIndices = new Map();
701
- const hourlyMetadataEntries = new Map();
702
- const existingHourlyMetadataEntries = new Map();
703
- const imageIds = new Set();
704
- for (let page = 1; page <= maxPages; page++) {
705
- const images = await (0, api_1.listImages)(page, 100);
706
- if (images.length === 0)
707
- break;
708
- let reachedLimit = false;
709
- for (const img of images) {
710
- const createdAt = new Date(img.created_at);
711
- if (Number.isNaN(createdAt.getTime()))
712
- continue;
713
- if (createdAt > targetDate.end)
714
- continue;
715
- if (createdAt < targetDate.start) {
716
- reachedLimit = true;
717
- break;
718
- }
719
- const dateParts = toDateParts(createdAt);
720
- const bucketKey = buildHourlyBucketKey(dateParts.year, dateParts.month, dateParts.day, dateParts.hour);
721
- if (!hourlyIndices.has(bucketKey)) {
722
- hourlyIndices.set(bucketKey, new Set());
723
- }
724
- if (!hourlyMetadataEntries.has(bucketKey)) {
725
- hourlyMetadataEntries.set(bucketKey, new Map());
726
- }
727
- hourlyIndices.get(bucketKey)?.add(img.image_id);
728
- imageIds.add(img.image_id);
729
- let merged = img;
730
- const cached = useCache ? (0, storage_1.loadImageCache)(img.image_id) : null;
731
- if (cached) {
732
- merged = mergeImageForDisplay(img, cached);
733
- }
734
- let values;
735
- let hasExistingMetadataEntry = false;
736
- if (useCache) {
737
- let existingForBucket = existingHourlyMetadataEntries.get(bucketKey);
738
- if (!existingForBucket) {
739
- existingForBucket = normalizeHourlyMetadataEntries((0, storage_1.loadHourlyMetadataCache)(metadataKind, dateParts.year, dateParts.month, dateParts.day, dateParts.hour));
740
- existingHourlyMetadataEntries.set(bucketKey, existingForBucket);
741
- }
742
- if (Object.prototype.hasOwnProperty.call(existingForBucket, img.image_id)) {
743
- values = existingForBucket[img.image_id];
744
- hasExistingMetadataEntry = true;
745
- }
746
- }
747
- if (!values) {
748
- values = normalizeRankingValues(extractValues(merged));
749
- }
750
- if (values.length === 0 && !hasExistingMetadataEntry) {
751
- try {
752
- const detail = await (0, api_1.getImageDetail)(img.image_id);
753
- merged = mergeImageForDisplay(merged, detail);
754
- values = normalizeRankingValues(extractValues(merged));
755
- }
756
- catch (_error) {
757
- // Keep best effort result when detail fetch fails.
758
- }
759
- }
760
- (0, storage_1.saveImageCache)(img.image_id, merged);
761
- hourlyMetadataEntries.get(bucketKey)?.set(img.image_id, values);
762
- }
763
- if (reachedLimit)
764
- break;
765
- }
766
- for (const [bucketKey, current] of hourlyIndices.entries()) {
767
- const { year, month, day, hour } = splitHourlyBucketKey(bucketKey);
768
- if (useCache) {
769
- const existing = (0, storage_1.loadHourlyCache)(year, month, day, hour) || [];
770
- for (const id of existing)
771
- current.add(id);
772
- }
773
- (0, storage_1.saveHourlyCache)(year, month, day, hour, Array.from(current));
774
- for (const id of current)
775
- imageIds.add(id);
776
- const mergedMetadataEntries = useCache
777
- ? normalizeHourlyMetadataEntries((0, storage_1.loadHourlyMetadataCache)(metadataKind, year, month, day, hour))
778
- : {};
779
- const currentMetadataEntries = hourlyMetadataEntries.get(bucketKey) || new Map();
780
- for (const [imageId, values] of currentMetadataEntries.entries()) {
781
- mergedMetadataEntries[imageId] = values;
782
- }
783
- (0, storage_1.saveHourlyMetadataCache)(metadataKind, year, month, day, hour, mergedMetadataEntries);
784
- }
785
- if (useCache) {
786
- const dates = getDatePartsInRange(targetDate.start, targetDate.end);
787
- const hours = getDateHourStrings();
788
- for (const date of dates) {
789
- for (const hour of hours) {
790
- const existing = (0, storage_1.loadHourlyCache)(date.year, date.month, date.day, hour) || [];
791
- for (const id of existing)
792
- imageIds.add(id);
793
- }
794
- }
795
- }
796
- return Array.from(imageIds);
797
- }
798
- function buildHourlyMetadataEntriesFromImageCache(year, month, day, hour, extractValues) {
799
- const imageIds = (0, storage_1.loadHourlyCache)(year, month, day, hour) || [];
800
- const valuesByImageId = {};
801
- for (const imageId of imageIds) {
802
- const image = (0, storage_1.loadImageCache)(imageId);
803
- if (!image)
804
- continue;
805
- valuesByImageId[imageId] = normalizeRankingValues(extractValues(image));
806
- }
807
- return valuesByImageId;
808
- }
809
- function loadOrBuildHourlyMetadataEntries(metadataKind, year, month, day, hour, extractValues) {
810
- const rawCached = (0, storage_1.loadHourlyMetadataCache)(metadataKind, year, month, day, hour);
811
- if (rawCached !== null) {
812
- return normalizeHourlyMetadataEntries(rawCached);
813
- }
814
- const built = buildHourlyMetadataEntriesFromImageCache(year, month, day, hour, extractValues);
815
- const hasHourlyIndex = Boolean((0, storage_1.loadHourlyCache)(year, month, day, hour));
816
- if (hasHourlyIndex || Object.keys(built).length > 0) {
817
- (0, storage_1.saveHourlyMetadataCache)(metadataKind, year, month, day, hour, built);
818
- }
819
- return built;
820
- }
821
- function aggregateRankingFromHourlyMetadataCache(targetDate, metadataKind, extractValues) {
822
- const counts = new Map();
823
- const seenImageIds = new Set();
824
- let totalImages = 0;
825
- let imageCountWithValues = 0;
826
- let totalAssignments = 0;
827
- const dates = getDatePartsInRange(targetDate.start, targetDate.end);
828
- const hours = getDateHourStrings();
829
- for (const date of dates) {
830
- for (const hour of hours) {
831
- const entries = loadOrBuildHourlyMetadataEntries(metadataKind, date.year, date.month, date.day, hour, extractValues);
832
- for (const [imageId, values] of Object.entries(entries)) {
833
- if (seenImageIds.has(imageId))
834
- continue;
835
- seenImageIds.add(imageId);
836
- totalImages++;
837
- if (values.length === 0)
838
- continue;
839
- imageCountWithValues++;
840
- totalAssignments += values.length;
841
- for (const value of values) {
842
- counts.set(value, (counts.get(value) || 0) + 1);
843
- }
844
- }
845
- }
846
- }
847
- const ranking = Array.from(counts.entries())
848
- .map(([key, count]) => ({ key, count }))
849
- .sort((a, b) => {
850
- if (b.count !== a.count)
851
- return b.count - a.count;
852
- return a.key.localeCompare(b.key);
853
- });
854
- return {
855
- ranking,
856
- totalImages,
857
- imageCountWithValues,
858
- totalAssignments,
859
- };
860
- }
861
- function buildAppsRankingFromCache(imageIds) {
862
- const counts = new Map();
863
- for (const imageId of imageIds) {
864
- const image = (0, storage_1.loadImageCache)(imageId);
865
- const apps = extractImageApps(image);
866
- if (apps.length === 0)
867
- continue;
868
- const app = apps[0];
869
- counts.set(app, (counts.get(app) || 0) + 1);
870
- }
871
- return Array.from(counts.entries())
872
- .map(([app, count]) => ({ app, count }))
873
- .sort((a, b) => {
874
- if (b.count !== a.count)
875
- return b.count - a.count;
876
- return a.app.localeCompare(b.app);
877
- });
878
- }
879
- function buildAppsRankingFromHourlyCache(targetDate) {
880
- const summary = aggregateRankingFromHourlyMetadataCache(targetDate, 'apps', extractImageApps);
881
- return {
882
- ranking: summary.ranking.map(item => ({ app: item.key, count: item.count })),
883
- totalImages: summary.totalImages,
884
- imageCountWithApps: summary.imageCountWithValues,
885
- };
886
- }
887
- function buildDomainsRankingFromCache(imageIds) {
888
- const counts = new Map();
889
- for (const imageId of imageIds) {
890
- const image = (0, storage_1.loadImageCache)(imageId);
891
- const domains = extractImageDomains(image);
892
- if (domains.length === 0)
893
- continue;
894
- const domain = domains[0];
895
- counts.set(domain, (counts.get(domain) || 0) + 1);
896
- }
897
- return Array.from(counts.entries())
898
- .map(([domain, count]) => ({ domain, count }))
899
- .sort((a, b) => {
900
- if (b.count !== a.count)
901
- return b.count - a.count;
902
- return a.domain.localeCompare(b.domain);
903
- });
904
- }
905
- function buildDomainsRankingFromHourlyCache(targetDate) {
906
- const summary = aggregateRankingFromHourlyMetadataCache(targetDate, 'domains', extractImageDomains);
907
- return {
908
- ranking: summary.ranking.map(item => ({ domain: item.key, count: item.count })),
909
- totalImages: summary.totalImages,
910
- imageCountWithDomains: summary.imageCountWithValues,
911
- };
912
- }
913
- function buildLocationsRankingFromCache(imageIds) {
914
- const counts = new Map();
915
- for (const imageId of imageIds) {
916
- const image = (0, storage_1.loadImageCache)(imageId);
917
- const locations = extractImageLocations(image);
918
- if (locations.length === 0)
919
- continue;
920
- const location = locations[0];
921
- counts.set(location, (counts.get(location) || 0) + 1);
922
- }
923
- return Array.from(counts.entries())
924
- .map(([location, count]) => ({ location, count }))
925
- .sort((a, b) => {
926
- if (b.count !== a.count)
927
- return b.count - a.count;
928
- return a.location.localeCompare(b.location);
929
- });
930
- }
931
- function buildLocationsRankingFromHourlyCache(targetDate) {
932
- const summary = aggregateRankingFromHourlyMetadataCache(targetDate, 'locations', extractImageLocations);
933
- return {
934
- ranking: summary.ranking.map(item => ({ location: item.key, count: item.count })),
935
- totalImages: summary.totalImages,
936
- imageCountWithLocations: summary.imageCountWithValues,
937
- };
938
- }
939
- function buildTagsRankingFromCache(imageIds) {
940
- const counts = new Map();
941
- let imageCountWithTags = 0;
942
- let totalTagAssignments = 0;
943
- for (const imageId of imageIds) {
944
- const image = (0, storage_1.loadImageCache)(imageId);
945
- const tags = extractImageTags(image);
946
- if (tags.length === 0)
947
- continue;
948
- imageCountWithTags++;
949
- for (const tag of tags) {
950
- counts.set(tag, (counts.get(tag) || 0) + 1);
951
- totalTagAssignments++;
952
- }
953
- }
954
- const ranking = Array.from(counts.entries())
955
- .map(([tag, count]) => ({ tag, count }))
956
- .sort((a, b) => {
957
- if (b.count !== a.count)
958
- return b.count - a.count;
959
- return a.tag.localeCompare(b.tag);
960
- });
961
- return {
962
- ranking,
963
- imageCountWithTags,
964
- totalTagAssignments,
965
- };
966
- }
967
- function buildTagsRankingFromHourlyCache(targetDate) {
968
- const summary = aggregateRankingFromHourlyMetadataCache(targetDate, 'tags', extractImageTags);
969
- return {
970
- ranking: summary.ranking.map(item => ({ tag: item.key, count: item.count })),
971
- totalImages: summary.totalImages,
972
- imageCountWithTags: summary.imageCountWithValues,
973
- totalTagAssignments: summary.totalAssignments,
974
- };
975
- }
976
- function buildUploadTimeSummaryFromHourlyCache(targetDate) {
977
- const seen = new Set();
978
- const hourCounts = Array.from({ length: 24 }, (_, hour) => ({ hour, count: 0 }));
979
- const weekdayCounts = Array.from({ length: 7 }, (_, weekday) => ({ weekday, count: 0 }));
980
- const dates = getDatePartsInRange(targetDate.start, targetDate.end);
981
- const hours = getDateHourStrings();
982
- for (const date of dates) {
983
- for (const hourText of hours) {
984
- const hour = Number(hourText);
985
- const imageIds = (0, storage_1.loadHourlyCache)(date.year, date.month, date.day, hourText) || [];
986
- const weekday = new Date(Number(date.year), Number(date.month) - 1, Number(date.day), hour, 0, 0, 0).getDay();
987
- for (const imageId of imageIds) {
988
- if (seen.has(imageId))
989
- continue;
990
- seen.add(imageId);
991
- hourCounts[hour].count++;
992
- weekdayCounts[weekday].count++;
993
- }
994
- }
995
- }
996
- return {
997
- totalImages: seen.size,
998
- byHour: hourCounts.sort((a, b) => {
999
- if (b.count !== a.count)
1000
- return b.count - a.count;
1001
- return a.hour - b.hour;
1002
- }),
1003
- byWeekday: weekdayCounts.sort((a, b) => {
1004
- if (b.count !== a.count)
1005
- return b.count - a.count;
1006
- return a.weekday - b.weekday;
1007
- }),
1008
- };
1009
- }
1010
- function buildUploadTimeSummaryFromImageCache(imageIds, targetDate) {
1011
- const seen = new Set();
1012
- const hourCounts = Array.from({ length: 24 }, (_, hour) => ({ hour, count: 0 }));
1013
- const weekdayCounts = Array.from({ length: 7 }, (_, weekday) => ({ weekday, count: 0 }));
1014
- for (const imageId of imageIds) {
1015
- if (seen.has(imageId))
1016
- continue;
1017
- const image = (0, storage_1.loadImageCache)(imageId);
1018
- const createdAtText = normalizeText(image?.created_at);
1019
- if (!createdAtText)
1020
- continue;
1021
- const createdAt = new Date(createdAtText);
1022
- if (Number.isNaN(createdAt.getTime()))
1023
- continue;
1024
- if (createdAt < targetDate.start || createdAt > targetDate.end)
1025
- continue;
1026
- seen.add(imageId);
1027
- hourCounts[createdAt.getHours()].count++;
1028
- weekdayCounts[createdAt.getDay()].count++;
1029
- }
1030
- return {
1031
- totalImages: seen.size,
1032
- byHour: hourCounts.sort((a, b) => {
1033
- if (b.count !== a.count)
1034
- return b.count - a.count;
1035
- return a.hour - b.hour;
1036
- }),
1037
- byWeekday: weekdayCounts.sort((a, b) => {
1038
- if (b.count !== a.count)
1039
- return b.count - a.count;
1040
- return a.weekday - b.weekday;
1041
- }),
1042
- };
1043
- }
1044
- function buildDailyUploadCountsFromHourlyCache(targetDate) {
1045
- const dates = getDatePartsInRange(targetDate.start, targetDate.end);
1046
- const hours = getDateHourStrings();
1047
- const byDate = new Map();
1048
- for (const date of dates) {
1049
- const dateLabel = `${date.year}-${date.month}-${date.day}`;
1050
- if (!byDate.has(dateLabel)) {
1051
- byDate.set(dateLabel, new Set());
1052
- }
1053
- const ids = byDate.get(dateLabel);
1054
- for (const hour of hours) {
1055
- const imageIds = (0, storage_1.loadHourlyCache)(date.year, date.month, date.day, hour) || [];
1056
- for (const imageId of imageIds)
1057
- ids.add(imageId);
1058
- }
1059
- }
1060
- return dates.map(date => {
1061
- const dateLabel = `${date.year}-${date.month}-${date.day}`;
1062
- return {
1063
- date: dateLabel,
1064
- count: byDate.get(dateLabel)?.size || 0,
1065
- };
1066
- });
1067
- }
1068
- function buildDailyUploadCountsFromImageCache(imageIds, targetDate) {
1069
- const dates = getDatePartsInRange(targetDate.start, targetDate.end);
1070
- const byDate = new Map();
1071
- for (const date of dates) {
1072
- const dateLabel = `${date.year}-${date.month}-${date.day}`;
1073
- byDate.set(dateLabel, new Set());
1074
- }
1075
- for (const imageId of imageIds) {
1076
- const image = (0, storage_1.loadImageCache)(imageId);
1077
- const createdAtText = normalizeText(image?.created_at);
1078
- if (!createdAtText)
1079
- continue;
1080
- const createdAt = new Date(createdAtText);
1081
- if (Number.isNaN(createdAt.getTime()))
1082
- continue;
1083
- if (createdAt < targetDate.start || createdAt > targetDate.end)
1084
- continue;
1085
- const dateLabel = formatDateYmd(createdAt);
1086
- const ids = byDate.get(dateLabel);
1087
- if (!ids)
1088
- continue;
1089
- ids.add(imageId);
1090
- }
1091
- return dates.map(date => {
1092
- const dateLabel = `${date.year}-${date.month}-${date.day}`;
1093
- return {
1094
- date: dateLabel,
1095
- count: byDate.get(dateLabel)?.size || 0,
1096
- };
1097
- });
1098
- }
1099
- function buildDailySummariesFromImageCache(targetDate) {
1100
- const dates = getDatePartsInRange(targetDate.start, targetDate.end);
1101
- const hours = getDateHourStrings();
1102
- const summaries = [];
1103
- for (const date of dates) {
1104
- const dateLabel = `${date.year}-${date.month}-${date.day}`;
1105
- const imageIds = new Set();
1106
- for (const hour of hours) {
1107
- const ids = (0, storage_1.loadHourlyCache)(date.year, date.month, date.day, hour) || [];
1108
- for (const id of ids)
1109
- imageIds.add(id);
1110
- }
1111
- const appCounts = new Map();
1112
- const domainCounts = new Map();
1113
- const tagCounts = new Map();
1114
- const locationCounts = new Map();
1115
- for (const imageId of imageIds) {
1116
- const image = (0, storage_1.loadImageCache)(imageId);
1117
- if (!image)
1118
- continue;
1119
- for (const app of extractImageApps(image)) {
1120
- appCounts.set(app, (appCounts.get(app) || 0) + 1);
1121
- }
1122
- for (const domain of extractImageDomains(image)) {
1123
- domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
1124
- }
1125
- for (const tag of extractImageTags(image)) {
1126
- tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
1127
- }
1128
- for (const location of extractImageLocations(image)) {
1129
- locationCounts.set(location, (locationCounts.get(location) || 0) + 1);
1130
- }
1131
- }
1132
- const sortEntries = (a, b) => {
1133
- if (b[1] !== a[1])
1134
- return b[1] - a[1];
1135
- return a[0].localeCompare(b[0]);
1136
- };
1137
- const apps = Array.from(appCounts.entries())
1138
- .sort(sortEntries)
1139
- .map(([app, count]) => ({ app, count }));
1140
- const domains = Array.from(domainCounts.entries())
1141
- .sort(sortEntries)
1142
- .map(([domain, count]) => ({ domain, count }));
1143
- const tags = Array.from(tagCounts.entries())
1144
- .sort(sortEntries)
1145
- .map(([tag, count]) => ({ tag, count }));
1146
- const locations = Array.from(locationCounts.entries())
1147
- .sort(sortEntries)
1148
- .map(([location, count]) => ({ location, count }));
1149
- summaries.push({
1150
- date: dateLabel,
1151
- imageCount: imageIds.size,
1152
- apps,
1153
- domains,
1154
- tags,
1155
- locations,
1156
- });
1157
- }
1158
- return summaries;
1159
- }
1160
- function appendStatsRankSection(lines, title, rows, top) {
1161
- lines.push(`### ${title}`);
1162
- const filtered = rows.filter(row => row.count > 0).slice(0, top);
1163
- if (filtered.length === 0) {
1164
- lines.push('- No data');
1165
- lines.push('');
1166
- return;
1167
- }
1168
- for (const row of filtered) {
1169
- lines.push(`- ${row.label}: ${row.count}`);
1170
- }
1171
- lines.push('');
1172
- }
1173
- function renderStatsMarkdown(params) {
1174
- const lines = [];
1175
- lines.push('## Gyazo Stats');
1176
- lines.push('');
1177
- lines.push(`- Window: ${params.startLabel} to ${params.endLabel} (${params.days} days)`);
1178
- lines.push(`- Total uploads: ${params.totalUploads}`);
1179
- lines.push('');
1180
- appendStatsRankSection(lines, 'Upload Time (Hour)', params.uploadTime.byHour.map(item => ({
1181
- label: `${String(item.hour).padStart(2, '0')}:00`,
1182
- count: item.count,
1183
- })), params.top);
1184
- appendStatsRankSection(lines, 'Upload Weekday', params.uploadTime.byWeekday.map(item => ({
1185
- label: WEEKDAY_LABELS[item.weekday] || String(item.weekday),
1186
- count: item.count,
1187
- })), Math.min(params.top, 7));
1188
- appendStatsRankSection(lines, 'Apps', params.apps.map(item => ({ label: item.app, count: item.count })), params.top);
1189
- appendStatsRankSection(lines, 'Domains', params.domains.map(item => ({ label: item.domain, count: item.count })), params.top);
1190
- appendStatsRankSection(lines, 'Tags', params.tags.map(item => ({ label: `#${item.tag}`, count: item.count })), params.top);
1191
- return lines.join('\n').trimEnd();
1192
- }
1193
- function renderSummaryText(params) {
1194
- const appendRankSection = (lines, title, rows, limit) => {
1195
- lines.push(`- ${title}:`);
1196
- const items = rows.filter(row => row.count > 0).slice(0, limit);
1197
- if (items.length === 0) {
1198
- lines.push(' - (none)');
1199
- return;
1200
- }
1201
- for (const row of items) {
1202
- lines.push(` - ${row.label}${row.count > 1 ? ` (${row.count})` : ''}`);
1203
- }
1204
- };
1205
- const lines = [];
1206
- lines.push('## Gyazo Summary');
1207
- lines.push('');
1208
- lines.push(`- Window: ${params.dateKey}`);
1209
- lines.push('');
1210
- for (const day of params.dailySummaries) {
1211
- lines.push(`### ${day.date}`);
1212
- lines.push(`- Image count: ${day.imageCount}`);
1213
- appendRankSection(lines, 'Apps', day.apps.map(item => ({ label: item.app, count: item.count })), params.limit);
1214
- appendRankSection(lines, 'Domains', day.domains.map(item => ({ label: item.domain, count: item.count })), params.limit);
1215
- appendRankSection(lines, 'Tags', day.tags.map(item => ({ label: `#${item.tag}`, count: item.count })), params.limit);
1216
- appendRankSection(lines, 'Locations', day.locations.map(item => ({ label: item.location, count: item.count })), params.limit);
1217
- lines.push('');
1218
- }
1219
- return lines.join('\n').trimEnd();
1220
- }
1221
- async function readStdinBuffer() {
1222
- return new Promise((resolve, reject) => {
1223
- const chunks = [];
1224
- process.stdin.on('data', (chunk) => {
1225
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1226
- });
1227
- process.stdin.on('end', () => resolve(Buffer.concat(chunks)));
1228
- process.stdin.on('error', reject);
1229
99
  });
1230
100
  }
1231
- function printGetMarkdown(image, ocrDescription, objects = []) {
1232
- const lines = [];
1233
- lines.push('## Gyazo Image');
1234
- lines.push('');
1235
- lines.push(`- URL: <${image.permalink_url}>`);
1236
- lines.push(`- Created at: ${formatCreatedAt(image.created_at)}`);
1237
- const title = normalizeText(image.metadata?.title);
1238
- if (title)
1239
- lines.push(`- Title: ${title}`);
1240
- const address = extractImageAddressText(image);
1241
- if (address)
1242
- lines.push(`- Address: ${address}`);
1243
- const altText = normalizeText(image.alt_text);
1244
- if (altText)
1245
- lines.push(`- Alt text: ${altText}`);
1246
- if (objects.length > 0) {
1247
- lines.push('');
1248
- lines.push('### Objects');
1249
- for (const object of objects) {
1250
- lines.push(`- ${formatObjectAnnotationLine(object)}`);
1251
- }
1252
- }
1253
- if (ocrDescription) {
1254
- const preview = buildOcrPreview(ocrDescription, 5);
1255
- lines.push('');
1256
- lines.push('### OCR');
1257
- lines.push('```text');
1258
- lines.push(preview.text);
1259
- lines.push('```');
1260
- if (preview.truncated) {
1261
- lines.push('');
1262
- lines.push(`> Truncated to first 5 lines. Use \`gyazo get --ocr ${image.image_id}\` for full text.`);
1263
- }
1264
- }
1265
- console.log(lines.join('\n'));
1266
- }
1267
- function summarizeImageForList(img) {
1268
- const domain = extractDomain(normalizeText(img.metadata?.url));
1269
- const cleanedTitle = sanitizeSummaryText(img.metadata?.title, domain);
1270
- const cleanedDesc = sanitizeSummaryText(img.metadata?.desc, domain);
1271
- const locationLabel = sanitizeSummaryText(extractImageLocationLabel(img));
1272
- const cleanedAltText = sanitizeSummaryText(img.alt_text);
1273
- let main = '(no title/description)';
1274
- if (cleanedTitle && cleanedDesc) {
1275
- main = `${cleanedTitle} | ${cleanedDesc}`;
1276
- }
1277
- else if (cleanedTitle) {
1278
- main = cleanedTitle;
1279
- }
1280
- else if (cleanedDesc) {
1281
- main = cleanedDesc;
1282
- }
1283
- if (cleanedAltText) {
1284
- if (main === '(no title/description)') {
1285
- main = cleanedAltText;
1286
- }
1287
- else if (cleanedAltText !== main) {
1288
- main = `${main} | alt: ${cleanedAltText}`;
1289
- }
1290
- }
1291
- const prefixes = [];
1292
- if (domain)
1293
- prefixes.push(`[${domain}]`);
1294
- if (locationLabel)
1295
- prefixes.push(`[${locationLabel}]`);
1296
- if (main === '(no title/description)') {
1297
- if (prefixes.length > 0)
1298
- return prefixes.join(' ');
1299
- return main;
1300
- }
1301
- if (prefixes.length > 0) {
1302
- return `${prefixes.join(' ')} ${main}`;
1303
- }
1304
- return main;
1305
- }
1306
- function shouldEnrichForLocationDisplay(img) {
1307
- const locationLabel = sanitizeSummaryText(extractImageLocationLabel(img));
1308
- return !locationLabel;
1309
- }
1310
- function mergeImageForDisplay(base, detail) {
1311
- return {
1312
- ...base,
1313
- ...detail,
1314
- metadata: {
1315
- ...(base?.metadata || {}),
1316
- ...(detail?.metadata || {}),
1317
- },
1318
- ocr: detail?.ocr ?? base?.ocr,
1319
- };
1320
- }
1321
- function cacheSearchResultImages(images) {
1322
- for (const img of images) {
1323
- if (!img?.image_id)
1324
- continue;
1325
- (0, storage_1.saveSearchImageCache)(img.image_id, img);
1326
- }
1327
- }
1328
- function supplementAltTextFromSearchCache(image, useCache = true) {
1329
- const hasAltText = Boolean(normalizeText(image.alt_text));
1330
- if (hasAltText)
1331
- return { image, supplemented: false };
1332
- if (!useCache)
1333
- return { image, supplemented: false };
1334
- const cached = (0, storage_1.loadSearchImageCache)(image.image_id);
1335
- const cachedAltText = normalizeText(cached?.alt_text);
1336
- const cachedHasAltText = Boolean(cachedAltText);
1337
- if (!cachedHasAltText)
1338
- return { image, supplemented: false };
1339
- return {
1340
- image: {
1341
- ...image,
1342
- alt_text: cachedAltText,
1343
- },
1344
- supplemented: true,
1345
- };
1346
- }
1347
- function supplementAltTextForDisplay(images, useCache = true) {
1348
- return images.map(img => supplementAltTextFromSearchCache(img, useCache).image);
101
+ else {
102
+ program.parseAsync(expandImplicitCommand(process.argv));
1349
103
  }
1350
- async function prepareImagesForDisplay(images, options = {}) {
1351
- const useCache = options.useCache !== false;
1352
- if (options.cacheSearchResults) {
1353
- cacheSearchResultImages(images);
1354
- }
1355
- let prepared = images;
1356
- if (options.enrichLocation) {
1357
- prepared = await enrichImagesForLocationDisplay(prepared, useCache);
1358
- }
1359
- prepared = supplementAltTextForDisplay(prepared, useCache);
1360
- return prepared;
1361
- }
1362
- async function enrichImagesForLocationDisplay(images, useCache = true) {
1363
- const enriched = [];
1364
- for (const img of images) {
1365
- let current = img;
1366
- if (!shouldEnrichForLocationDisplay(current)) {
1367
- enriched.push(current);
1368
- continue;
1369
- }
1370
- if (useCache) {
1371
- const cached = (0, storage_1.loadImageCache)(img.image_id);
1372
- if (cached) {
1373
- current = mergeImageForDisplay(current, cached);
1374
- }
1375
- }
1376
- if (!shouldEnrichForLocationDisplay(current)) {
1377
- enriched.push(current);
1378
- continue;
1379
- }
1380
- try {
1381
- const detail = await (0, api_1.getImageDetail)(img.image_id);
1382
- (0, storage_1.saveImageCache)(img.image_id, detail);
1383
- current = mergeImageForDisplay(current, detail);
1384
- }
1385
- catch (_error) {
1386
- // Keep current data when detail fetch fails.
1387
- }
1388
- enriched.push(current);
1389
- }
1390
- return enriched;
1391
- }
1392
- function printListImages(images) {
1393
- images.forEach(img => {
1394
- const summary = truncateText(summarizeImageForList(img), 120);
1395
- const created = formatCreatedAt(img.created_at);
1396
- const shortId = shortenImageId(img.image_id);
1397
- const imageUrl = img.permalink_url || `https://gyazo.com/${img.image_id}`;
1398
- const linkedId = formatTerminalLink(shortId, imageUrl);
1399
- console.log(`- [${created}] ${summary} (id: ${linkedId})`);
1400
- });
1401
- }
1402
- program
1403
- .command('list')
1404
- .alias('ls')
1405
- .description('List recent images')
1406
- .option('-p, --page <number>', 'page number', '1')
1407
- .option('-l, --limit <number>', 'items per page', '20')
1408
- .option('-j, --json', 'output as JSON')
1409
- .option('-H, --hour <yyyy-mm-dd-hh>', 'target hour')
1410
- .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'target date/range')
1411
- .option('--today', 'target today only')
1412
- .option('--max-pages <number>', 'max pages to scan for --date/--today mode', '100')
1413
- .option('--photos', 'alias of search "has:location"')
1414
- .option('--uploaded', 'alias of search "gyazocli_uploads"')
1415
- .option('--no-cache', 'force fetch from API')
1416
- .action(async (options) => {
1417
- await (0, credentials_1.ensureAccessToken)();
1418
- try {
1419
- const useCache = options.cache !== false;
1420
- const page = parsePositiveIntegerOption(options.page, '--page');
1421
- const limit = parsePositiveIntegerOption(options.limit, '--limit');
1422
- const maxPages = parsePositiveIntegerOption(options.maxPages, '--max-pages');
1423
- const hasDateRange = Boolean(options.date || options.today);
1424
- const targetDate = hasDateRange
1425
- ? (options.today ? parseDateOption() : parseDateOption(options.date))
1426
- : undefined;
1427
- if (options.photos && options.uploaded) {
1428
- console.error('Error: --photos and --uploaded cannot be used together.');
1429
- process.exit(1);
1430
- }
1431
- if (options.today && options.date) {
1432
- console.error('Error: --today and --date cannot be used together.');
1433
- process.exit(1);
1434
- }
1435
- if ((options.photos || options.uploaded) && options.hour) {
1436
- console.error('Error: --photos/--uploaded and --hour cannot be used together.');
1437
- process.exit(1);
1438
- }
1439
- if (options.hour && hasDateRange) {
1440
- console.error('Error: --hour and --date/--today cannot be used together.');
1441
- process.exit(1);
1442
- }
1443
- const aliasQuery = options.photos
1444
- ? 'has:location'
1445
- : options.uploaded
1446
- ? 'gyazocli_uploads'
1447
- : undefined;
1448
- if (aliasQuery) {
1449
- let images = [];
1450
- if (targetDate) {
1451
- const collected = [];
1452
- for (let searchPage = 1; searchPage <= maxPages; searchPage++) {
1453
- const pageImages = await (0, api_1.searchImages)(aliasQuery, searchPage, 100);
1454
- if (pageImages.length === 0)
1455
- break;
1456
- let reachedLimit = false;
1457
- for (const img of pageImages) {
1458
- const createdAt = new Date(img.created_at);
1459
- if (Number.isNaN(createdAt.getTime()))
1460
- continue;
1461
- if (createdAt > targetDate.end)
1462
- continue;
1463
- if (createdAt < targetDate.start) {
1464
- reachedLimit = true;
1465
- break;
1466
- }
1467
- collected.push(img);
1468
- }
1469
- if (reachedLimit)
1470
- break;
1471
- }
1472
- collected.sort((a, b) => {
1473
- const ta = new Date(a.created_at).getTime();
1474
- const tb = new Date(b.created_at).getTime();
1475
- return tb - ta;
1476
- });
1477
- const startIndex = (page - 1) * limit;
1478
- images = collected.slice(startIndex, startIndex + limit);
1479
- }
1480
- else {
1481
- images = await (0, api_1.searchImages)(aliasQuery, page, limit);
1482
- }
1483
- if (options.json) {
1484
- console.log(JSON.stringify(images, null, 2));
1485
- }
1486
- else {
1487
- const imagesForDisplay = await prepareImagesForDisplay(images, {
1488
- cacheSearchResults: true,
1489
- enrichLocation: true,
1490
- useCache,
1491
- });
1492
- printListImages(imagesForDisplay);
1493
- }
1494
- return;
1495
- }
1496
- if (targetDate) {
1497
- let imageIds = [];
1498
- if (useCache) {
1499
- imageIds = loadImageIdsFromDateRangeCache(targetDate);
1500
- if (imageIds.length === 0) {
1501
- await warmDateCacheForList(targetDate, maxPages, true);
1502
- imageIds = loadImageIdsFromDateRangeCache(targetDate);
1503
- }
1504
- }
1505
- else {
1506
- imageIds = await warmDateCacheForList(targetDate, maxPages, false);
1507
- }
1508
- if (imageIds.length === 0) {
1509
- console.log(`No images found for ${targetDate.dateKey}.`);
1510
- return;
1511
- }
1512
- let images = imageIds
1513
- .map(id => (0, storage_1.loadImageCache)(id))
1514
- .filter((img) => img !== null);
1515
- images = images.filter((img) => {
1516
- const createdAt = new Date(img.created_at);
1517
- if (Number.isNaN(createdAt.getTime()))
1518
- return false;
1519
- return createdAt >= targetDate.start && createdAt <= targetDate.end;
1520
- });
1521
- images.sort((a, b) => {
1522
- const ta = new Date(a.created_at).getTime();
1523
- const tb = new Date(b.created_at).getTime();
1524
- return tb - ta;
1525
- });
1526
- const startIndex = (page - 1) * limit;
1527
- const pageImages = images.slice(startIndex, startIndex + limit);
1528
- if (options.json) {
1529
- console.log(JSON.stringify(pageImages, null, 2));
1530
- }
1531
- else {
1532
- const imagesForDisplay = await prepareImagesForDisplay(pageImages, {
1533
- enrichLocation: true,
1534
- useCache,
1535
- });
1536
- printListImages(imagesForDisplay);
1537
- }
1538
- return;
1539
- }
1540
- if (options.hour) {
1541
- const parts = options.hour.split('-');
1542
- if (parts.length !== 4) {
1543
- console.error('Error: hour format must be yyyy-mm-dd-hh');
1544
- process.exit(1);
1545
- }
1546
- const [year, month, day, hour] = parts;
1547
- const imageIds = (0, storage_1.loadHourlyCache)(year, month, day, hour);
1548
- if (!imageIds) {
1549
- console.log(`No images found for ${options.hour} in cache.`);
1550
- return;
1551
- }
1552
- let images = [];
1553
- if (useCache) {
1554
- images = imageIds.map(id => (0, storage_1.loadImageCache)(id)).filter(img => img !== null);
1555
- }
1556
- else {
1557
- for (const imageId of imageIds) {
1558
- try {
1559
- const detail = await (0, api_1.getImageDetail)(imageId);
1560
- (0, storage_1.saveImageCache)(imageId, detail);
1561
- images.push(detail);
1562
- }
1563
- catch (_error) {
1564
- // Skip failed items and continue with the rest.
1565
- }
1566
- }
1567
- }
1568
- if (options.json) {
1569
- console.log(JSON.stringify(images, null, 2));
1570
- }
1571
- else {
1572
- const imagesForDisplay = await prepareImagesForDisplay(images, {
1573
- enrichLocation: true,
1574
- useCache,
1575
- });
1576
- printListImages(imagesForDisplay);
1577
- }
1578
- return;
1579
- }
1580
- const images = await (0, api_1.listImages)(page, limit);
1581
- if (options.json) {
1582
- console.log(JSON.stringify(images, null, 2));
1583
- }
1584
- else {
1585
- const imagesForDisplay = await prepareImagesForDisplay(images, {
1586
- enrichLocation: true,
1587
- useCache,
1588
- });
1589
- printListImages(imagesForDisplay);
1590
- }
1591
- }
1592
- catch (error) {
1593
- console.error('Error listing images:', error.message);
1594
- }
1595
- });
1596
- program
1597
- .command('get <image_id>')
1598
- .description('Get detailed metadata for an image')
1599
- .option('-j, --json', 'output as JSON')
1600
- .option('--ocr', 'output OCR text only')
1601
- .option('--objects', 'output object annotations only')
1602
- .option('--no-cache', 'force fetch from API')
1603
- .action(async (imageId, options) => {
1604
- await (0, credentials_1.ensureAccessToken)();
1605
- try {
1606
- if (options.json && (options.ocr || options.objects)) {
1607
- console.error('Error: --json cannot be used with --ocr or --objects.');
1608
- process.exit(1);
1609
- }
1610
- if (options.ocr && options.objects) {
1611
- console.error('Error: --ocr and --objects cannot be used together.');
1612
- process.exit(1);
1613
- }
1614
- let image = options.cache !== false ? (0, storage_1.loadImageCache)(imageId) : null;
1615
- if (!image) {
1616
- image = await (0, api_1.getImageDetail)(imageId);
1617
- (0, storage_1.saveImageCache)(imageId, image);
1618
- }
1619
- const supplemented = supplementAltTextFromSearchCache(image);
1620
- image = supplemented.image;
1621
- if (supplemented.supplemented) {
1622
- (0, storage_1.saveImageCache)(imageId, image);
1623
- }
1624
- const ocrDescription = extractOcrDescription(image);
1625
- const objects = extractObjectAnnotations(image);
1626
- if (options.ocr) {
1627
- if (!ocrDescription) {
1628
- console.error('OCR not found for this image.');
1629
- process.exit(1);
1630
- }
1631
- console.log(ocrDescription);
1632
- return;
1633
- }
1634
- if (options.objects) {
1635
- if (objects.length === 0) {
1636
- console.error('Object annotations not found for this image.');
1637
- process.exit(1);
1638
- }
1639
- console.log(objects.map(formatObjectAnnotationLine).join('\n'));
1640
- return;
1641
- }
1642
- if (options.json) {
1643
- console.log(JSON.stringify(image, null, 2));
1644
- }
1645
- else {
1646
- printGetMarkdown(image, ocrDescription, objects);
1647
- }
1648
- }
1649
- catch (error) {
1650
- console.error('Error getting image:', error.message);
1651
- }
1652
- });
1653
- program
1654
- .command('search [query]')
1655
- .description('Search images')
1656
- .option('-j, --json', 'output as JSON')
1657
- .option('--no-cache', 'force fetch from API')
1658
- .action(async (query, options) => {
1659
- await (0, credentials_1.ensureAccessToken)();
1660
- try {
1661
- if (!normalizeText(query)) {
1662
- console.error('Error: Query is required.');
1663
- console.error('Hint: Run `gyazo search -h` for usage.');
1664
- process.exit(1);
1665
- }
1666
- const images = await (0, api_1.searchImages)(query);
1667
- const useCache = options.cache !== false;
1668
- if (options.json) {
1669
- cacheSearchResultImages(images);
1670
- console.log(JSON.stringify(images, null, 2));
1671
- }
1672
- else {
1673
- const imagesForDisplay = await prepareImagesForDisplay(images, {
1674
- cacheSearchResults: true,
1675
- enrichLocation: true,
1676
- useCache,
1677
- });
1678
- printListImages(imagesForDisplay);
1679
- }
1680
- }
1681
- catch (error) {
1682
- console.error('Error searching images:', error.message);
1683
- }
1684
- });
1685
- program
1686
- .command('apps')
1687
- .description('Rank metadata app names for a specific date')
1688
- .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'target date/range')
1689
- .option('--today', 'target today only (overrides default weekly range)')
1690
- .option('-l, --limit <number>', 'maximum ranking rows (max: 10)', '10')
1691
- .option('--max-pages <number>', 'max pages to scan before stopping', '10')
1692
- .option('-j, --json', 'output as JSON')
1693
- .option('--no-cache', 'force fetch from API')
1694
- .action(async (options) => {
1695
- await (0, credentials_1.ensureAccessToken)();
1696
- try {
1697
- const targetDate = resolveRankingRangeOption(options);
1698
- const requestedLimit = parsePositiveIntegerOption(options.limit, '--limit');
1699
- const limit = Math.min(requestedLimit, 10);
1700
- const maxPages = parsePositiveIntegerOption(options.maxPages, '--max-pages');
1701
- const useCache = options.cache !== false;
1702
- let ranking = [];
1703
- let totalWithApp = 0;
1704
- let totalImages = 0;
1705
- if (useCache) {
1706
- let cacheSummary = buildAppsRankingFromHourlyCache(targetDate);
1707
- if (cacheSummary.totalImages === 0) {
1708
- await warmDateCacheForApps(targetDate, maxPages, true);
1709
- cacheSummary = buildAppsRankingFromHourlyCache(targetDate);
1710
- }
1711
- ranking = cacheSummary.ranking;
1712
- totalWithApp = cacheSummary.imageCountWithApps;
1713
- totalImages = cacheSummary.totalImages;
1714
- }
1715
- else {
1716
- const imageIds = await warmDateCacheForApps(targetDate, maxPages, false);
1717
- ranking = buildAppsRankingFromCache(imageIds);
1718
- totalWithApp = ranking.reduce((sum, item) => sum + item.count, 0);
1719
- totalImages = imageIds.length;
1720
- }
1721
- const displayedRanking = ranking.slice(0, limit);
1722
- if (options.json) {
1723
- console.log(JSON.stringify({
1724
- date: targetDate.dateKey,
1725
- image_count: totalImages,
1726
- app_image_count: totalWithApp,
1727
- total_apps: ranking.length,
1728
- ranking: displayedRanking,
1729
- }, null, 2));
1730
- return;
1731
- }
1732
- if (ranking.length === 0) {
1733
- console.log(`No app metadata found for ${targetDate.dateKey}.`);
1734
- return;
1735
- }
1736
- console.log(`Apps on ${targetDate.dateKey}`);
1737
- displayedRanking.forEach((item, index) => {
1738
- console.log(`${index + 1}. ${item.app}: ${item.count}`);
1739
- });
1740
- console.log(`Total images with app metadata: ${totalWithApp}`);
1741
- }
1742
- catch (error) {
1743
- console.error('Error ranking apps:', error.message);
1744
- process.exit(1);
1745
- }
1746
- });
1747
- program
1748
- .command('domains')
1749
- .description('Rank metadata URL domains for a specific date')
1750
- .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'target date/range')
1751
- .option('--today', 'target today only (overrides default weekly range)')
1752
- .option('-l, --limit <number>', 'maximum ranking rows (max: 10)', '10')
1753
- .option('--max-pages <number>', 'max pages to scan before stopping', '10')
1754
- .option('-j, --json', 'output as JSON')
1755
- .option('--no-cache', 'force fetch from API')
1756
- .action(async (options) => {
1757
- await (0, credentials_1.ensureAccessToken)();
1758
- try {
1759
- const targetDate = resolveRankingRangeOption(options);
1760
- const requestedLimit = parsePositiveIntegerOption(options.limit, '--limit');
1761
- const limit = Math.min(requestedLimit, 10);
1762
- const maxPages = parsePositiveIntegerOption(options.maxPages, '--max-pages');
1763
- const useCache = options.cache !== false;
1764
- let ranking = [];
1765
- let totalWithDomain = 0;
1766
- let totalImages = 0;
1767
- if (useCache) {
1768
- let cacheSummary = buildDomainsRankingFromHourlyCache(targetDate);
1769
- if (cacheSummary.totalImages === 0) {
1770
- await warmDateCacheForDomains(targetDate, maxPages, true);
1771
- cacheSummary = buildDomainsRankingFromHourlyCache(targetDate);
1772
- }
1773
- ranking = cacheSummary.ranking;
1774
- totalWithDomain = cacheSummary.imageCountWithDomains;
1775
- totalImages = cacheSummary.totalImages;
1776
- }
1777
- else {
1778
- const imageIds = await warmDateCacheForDomains(targetDate, maxPages, false);
1779
- ranking = buildDomainsRankingFromCache(imageIds);
1780
- totalWithDomain = ranking.reduce((sum, item) => sum + item.count, 0);
1781
- totalImages = imageIds.length;
1782
- }
1783
- const displayedRanking = ranking.slice(0, limit);
1784
- if (options.json) {
1785
- console.log(JSON.stringify({
1786
- date: targetDate.dateKey,
1787
- image_count: totalImages,
1788
- domain_image_count: totalWithDomain,
1789
- total_domains: ranking.length,
1790
- ranking: displayedRanking,
1791
- }, null, 2));
1792
- return;
1793
- }
1794
- if (ranking.length === 0) {
1795
- console.log(`No domain metadata found for ${targetDate.dateKey}.`);
1796
- return;
1797
- }
1798
- console.log(`Domains on ${targetDate.dateKey}`);
1799
- displayedRanking.forEach((item, index) => {
1800
- console.log(`${index + 1}. ${item.domain}: ${item.count}`);
1801
- });
1802
- console.log(`Total images with domain metadata: ${totalWithDomain}`);
1803
- }
1804
- catch (error) {
1805
- console.error('Error ranking domains:', error.message);
1806
- process.exit(1);
1807
- }
1808
- });
1809
- program
1810
- .command('tags')
1811
- .description('Rank metadata tags for a specific date')
1812
- .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'target date/range')
1813
- .option('--today', 'target today only (overrides default weekly range)')
1814
- .option('-l, --limit <number>', 'maximum ranking rows (max: 10)', '10')
1815
- .option('--max-pages <number>', 'max pages to scan before stopping', '10')
1816
- .option('-j, --json', 'output as JSON')
1817
- .option('--no-cache', 'force fetch from API')
1818
- .action(async (options) => {
1819
- await (0, credentials_1.ensureAccessToken)();
1820
- try {
1821
- const targetDate = resolveRankingRangeOption(options);
1822
- const requestedLimit = parsePositiveIntegerOption(options.limit, '--limit');
1823
- const limit = Math.min(requestedLimit, 10);
1824
- const maxPages = parsePositiveIntegerOption(options.maxPages, '--max-pages');
1825
- const useCache = options.cache !== false;
1826
- let summary;
1827
- let totalImages = 0;
1828
- if (useCache) {
1829
- let cacheSummary = buildTagsRankingFromHourlyCache(targetDate);
1830
- if (cacheSummary.totalImages === 0) {
1831
- await warmDateCacheForTags(targetDate, maxPages, true);
1832
- cacheSummary = buildTagsRankingFromHourlyCache(targetDate);
1833
- }
1834
- summary = cacheSummary;
1835
- totalImages = cacheSummary.totalImages;
1836
- }
1837
- else {
1838
- const imageIds = await warmDateCacheForTags(targetDate, maxPages, false);
1839
- summary = buildTagsRankingFromCache(imageIds);
1840
- totalImages = imageIds.length;
1841
- }
1842
- const displayedRanking = summary.ranking.slice(0, limit);
1843
- if (options.json) {
1844
- console.log(JSON.stringify({
1845
- date: targetDate.dateKey,
1846
- image_count: totalImages,
1847
- image_count_with_tags: summary.imageCountWithTags,
1848
- total_tag_assignments: summary.totalTagAssignments,
1849
- total_tags: summary.ranking.length,
1850
- ranking: displayedRanking,
1851
- }, null, 2));
1852
- return;
1853
- }
1854
- if (summary.ranking.length === 0) {
1855
- console.log(`No tag metadata found for ${targetDate.dateKey}.`);
1856
- return;
1857
- }
1858
- console.log(`Tags on ${targetDate.dateKey}`);
1859
- displayedRanking.forEach((item, index) => {
1860
- console.log(`${index + 1}. #${item.tag}: ${item.count}`);
1861
- });
1862
- console.log(`Total images with tag metadata: ${summary.imageCountWithTags}`);
1863
- }
1864
- catch (error) {
1865
- console.error('Error ranking tags:', error.message);
1866
- process.exit(1);
1867
- }
1868
- });
1869
- program
1870
- .command('locations')
1871
- .description('Rank metadata locations for a specific date')
1872
- .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'target date/range')
1873
- .option('--today', 'target today only (overrides default weekly range)')
1874
- .option('-l, --limit <number>', 'maximum ranking rows (max: 10)', '10')
1875
- .option('--max-pages <number>', 'max pages to scan before stopping', '10')
1876
- .option('-j, --json', 'output as JSON')
1877
- .option('--no-cache', 'force fetch from API')
1878
- .action(async (options) => {
1879
- await (0, credentials_1.ensureAccessToken)();
1880
- try {
1881
- const targetDate = resolveRankingRangeOption(options);
1882
- const requestedLimit = parsePositiveIntegerOption(options.limit, '--limit');
1883
- const limit = Math.min(requestedLimit, 10);
1884
- const maxPages = parsePositiveIntegerOption(options.maxPages, '--max-pages');
1885
- const useCache = options.cache !== false;
1886
- let ranking = [];
1887
- let totalWithLocation = 0;
1888
- let totalImages = 0;
1889
- if (useCache) {
1890
- let cacheSummary = buildLocationsRankingFromHourlyCache(targetDate);
1891
- if (cacheSummary.totalImages === 0) {
1892
- await warmDateCacheForLocations(targetDate, maxPages, true);
1893
- cacheSummary = buildLocationsRankingFromHourlyCache(targetDate);
1894
- }
1895
- ranking = cacheSummary.ranking;
1896
- totalWithLocation = cacheSummary.imageCountWithLocations;
1897
- totalImages = cacheSummary.totalImages;
1898
- }
1899
- else {
1900
- const imageIds = await warmDateCacheForLocations(targetDate, maxPages, false);
1901
- ranking = buildLocationsRankingFromCache(imageIds);
1902
- totalWithLocation = ranking.reduce((sum, item) => sum + item.count, 0);
1903
- totalImages = imageIds.length;
1904
- }
1905
- const displayedRanking = ranking.slice(0, limit);
1906
- if (options.json) {
1907
- console.log(JSON.stringify({
1908
- date: targetDate.dateKey,
1909
- image_count: totalImages,
1910
- location_image_count: totalWithLocation,
1911
- total_locations: ranking.length,
1912
- ranking: displayedRanking,
1913
- }, null, 2));
1914
- return;
1915
- }
1916
- if (ranking.length === 0) {
1917
- console.log(`No location metadata found for ${targetDate.dateKey}.`);
1918
- return;
1919
- }
1920
- console.log(`Locations on ${targetDate.dateKey}`);
1921
- displayedRanking.forEach((item, index) => {
1922
- console.log(`${index + 1}. ${item.location}: ${item.count}`);
1923
- });
1924
- console.log(`Total images with location metadata: ${totalWithLocation}`);
1925
- }
1926
- catch (error) {
1927
- console.error('Error ranking locations:', error.message);
1928
- process.exit(1);
1929
- }
1930
- });
1931
- program
1932
- .command('summary')
1933
- .description('Show weekly summary with daily uploads and metadata rankings')
1934
- .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'target date/range')
1935
- .option('--today', 'target today only (overrides default weekly range)')
1936
- .option('-l, --limit <number>', 'maximum ranking rows (max: 10)', '10')
1937
- .option('--max-pages <number>', 'max pages to scan before stopping', '10')
1938
- .option('-j, --json', 'output as JSON')
1939
- .option('--no-cache', 'force fetch from API')
1940
- .action(async (options) => {
1941
- await (0, credentials_1.ensureAccessToken)();
1942
- try {
1943
- const targetDate = resolveRankingRangeOption(options);
1944
- const requestedLimit = parsePositiveIntegerOption(options.limit, '--limit');
1945
- const limit = Math.min(requestedLimit, 10);
1946
- const maxPages = parsePositiveIntegerOption(options.maxPages, '--max-pages');
1947
- const useCache = options.cache !== false;
1948
- let dailySummaries = [];
1949
- if (useCache) {
1950
- dailySummaries = buildDailySummariesFromImageCache(targetDate);
1951
- const totalUploads = dailySummaries.reduce((sum, day) => sum + day.imageCount, 0);
1952
- if (totalUploads === 0) {
1953
- await warmDateCacheForTags(targetDate, maxPages, true);
1954
- await warmDateCacheForLocations(targetDate, maxPages, true);
1955
- dailySummaries = buildDailySummariesFromImageCache(targetDate);
1956
- }
1957
- else {
1958
- const hasMetadata = dailySummaries.some(day => day.apps.length > 0 || day.domains.length > 0 || day.tags.length > 0 || day.locations.length > 0);
1959
- if (!hasMetadata) {
1960
- await warmDateCacheForTags(targetDate, maxPages, true);
1961
- await warmDateCacheForLocations(targetDate, maxPages, true);
1962
- dailySummaries = buildDailySummariesFromImageCache(targetDate);
1963
- }
1964
- }
1965
- }
1966
- else {
1967
- await warmDateCacheForTags(targetDate, maxPages, false);
1968
- await warmDateCacheForLocations(targetDate, maxPages, false);
1969
- dailySummaries = buildDailySummariesFromImageCache(targetDate);
1970
- }
1971
- if (options.json) {
1972
- console.log(JSON.stringify({
1973
- date: targetDate.dateKey,
1974
- days: dailySummaries.map(day => ({
1975
- date: day.date,
1976
- image_count: day.imageCount,
1977
- apps: day.apps.slice(0, limit),
1978
- domains: day.domains.slice(0, limit),
1979
- tags: day.tags.slice(0, limit),
1980
- locations: day.locations.slice(0, limit),
1981
- })),
1982
- }, null, 2));
1983
- return;
1984
- }
1985
- console.log(renderSummaryText({
1986
- dateKey: targetDate.dateKey,
1987
- dailySummaries,
1988
- limit,
1989
- }));
1990
- }
1991
- catch (error) {
1992
- console.error('Error building summary:', error.message);
1993
- process.exit(1);
1994
- }
1995
- });
1996
- program
1997
- .command('stats')
1998
- .description('Show weekly stats summary in Markdown')
1999
- .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'window end date anchor (default: yesterday)')
2000
- .option('--days <number>', 'window length in days', '7')
2001
- .option('--top <number>', 'rows per section', '10')
2002
- .option('--max-pages <number>', 'max pages to fetch when warming cache', '10')
2003
- .option('--no-cache', 'force fetch from API')
2004
- .action(async (options) => {
2005
- await (0, credentials_1.ensureAccessToken)();
2006
- try {
2007
- const { range, days, startLabel, endLabel } = buildStatsDateRange(options.date, options.days || '7');
2008
- const top = Math.min(parsePositiveIntegerOption(options.top, '--top'), 20);
2009
- const maxPages = parsePositiveIntegerOption(options.maxPages, '--max-pages');
2010
- const useCache = options.cache !== false;
2011
- let uploadTime;
2012
- let apps = [];
2013
- let domains = [];
2014
- let tags = [];
2015
- let totalUploads = 0;
2016
- if (useCache) {
2017
- uploadTime = buildUploadTimeSummaryFromHourlyCache(range);
2018
- if (uploadTime.totalImages === 0) {
2019
- await warmDateCacheForApps(range, maxPages, true);
2020
- uploadTime = buildUploadTimeSummaryFromHourlyCache(range);
2021
- }
2022
- let appsSummary = buildAppsRankingFromHourlyCache(range);
2023
- if (uploadTime.totalImages > 0 && appsSummary.totalImages === 0) {
2024
- await warmDateCacheForApps(range, maxPages, true);
2025
- appsSummary = buildAppsRankingFromHourlyCache(range);
2026
- }
2027
- let domainsSummary = buildDomainsRankingFromHourlyCache(range);
2028
- if (uploadTime.totalImages > 0 && domainsSummary.totalImages === 0) {
2029
- await warmDateCacheForDomains(range, maxPages, true);
2030
- domainsSummary = buildDomainsRankingFromHourlyCache(range);
2031
- }
2032
- let tagsSummary = buildTagsRankingFromHourlyCache(range);
2033
- if (uploadTime.totalImages > 0 && tagsSummary.totalImages === 0) {
2034
- await warmDateCacheForTags(range, maxPages, true);
2035
- tagsSummary = buildTagsRankingFromHourlyCache(range);
2036
- }
2037
- apps = appsSummary.ranking;
2038
- domains = domainsSummary.ranking;
2039
- tags = tagsSummary.ranking;
2040
- totalUploads = uploadTime.totalImages;
2041
- }
2042
- else {
2043
- const imageIds = await warmDateCacheForTags(range, maxPages, false);
2044
- uploadTime = buildUploadTimeSummaryFromImageCache(imageIds, range);
2045
- apps = buildAppsRankingFromCache(imageIds);
2046
- domains = buildDomainsRankingFromCache(imageIds);
2047
- tags = buildTagsRankingFromCache(imageIds).ranking;
2048
- totalUploads = uploadTime.totalImages;
2049
- }
2050
- console.log(renderStatsMarkdown({
2051
- startLabel,
2052
- endLabel,
2053
- days,
2054
- totalUploads,
2055
- uploadTime,
2056
- apps,
2057
- domains,
2058
- tags,
2059
- top,
2060
- }));
2061
- }
2062
- catch (error) {
2063
- console.error('Error building stats:', error.message);
2064
- process.exit(1);
2065
- }
2066
- });
2067
- program
2068
- .command('upload [path]')
2069
- .description('Upload an image file (or read image bytes from stdin)')
2070
- .option('--title <title>', 'image title')
2071
- .option('--app <app>', 'application name', 'gyazocli')
2072
- .option('--url <url>', 'source URL (sent as referer_url)')
2073
- .option('--timestamp <unix_timestamp>', 'created_at unix timestamp (current or past)')
2074
- .option('--desc <desc>', 'image description')
2075
- .action(async (inputPath, options) => {
2076
- await (0, credentials_1.ensureAccessToken)();
2077
- let imageData;
2078
- let filename = 'stdin-upload.bin';
2079
- if (inputPath && inputPath !== '-') {
2080
- const resolvedPath = path_1.default.resolve(inputPath);
2081
- if (!fs_1.default.existsSync(resolvedPath)) {
2082
- console.error(`Error: File not found: ${resolvedPath}`);
2083
- process.exit(1);
2084
- }
2085
- imageData = fs_1.default.readFileSync(resolvedPath);
2086
- filename = path_1.default.basename(resolvedPath);
2087
- }
2088
- else {
2089
- if (process.stdin.isTTY) {
2090
- console.error('Error: Provide an image path or pipe image data via stdin.');
2091
- console.error('Hint: Run `gyazo upload -h` for usage.');
2092
- process.exit(1);
2093
- }
2094
- imageData = await readStdinBuffer();
2095
- if (imageData.length === 0) {
2096
- console.error('Error: No image data received from stdin.');
2097
- process.exit(1);
2098
- }
2099
- }
2100
- const desc = ensureUploadDescTag(options.desc);
2101
- const timestamp = parseUploadTimestamp(options.timestamp);
2102
- try {
2103
- const uploaded = await (0, api_1.uploadImage)({
2104
- imageData,
2105
- filename,
2106
- title: options.title,
2107
- app: options.app || 'gyazocli',
2108
- refererUrl: options.url,
2109
- desc,
2110
- timestamp,
2111
- });
2112
- console.log(`URL: ${uploaded.permalink_url}`);
2113
- console.log(`ID: ${uploaded.image_id}`);
2114
- if (uploaded.created_at) {
2115
- console.log(`Created at: ${formatCreatedAt(uploaded.created_at)}`);
2116
- }
2117
- console.log(`App: ${options.app || 'gyazocli'}`);
2118
- console.log(`Desc: ${desc}`);
2119
- }
2120
- catch (error) {
2121
- console.error('Error uploading image:', error.message);
2122
- process.exit(1);
2123
- }
2124
- });
2125
- program
2126
- .command('sync')
2127
- .description('Sync images from yesterday back to N days')
2128
- .option('--days <number>', 'number of days to sync (used when --date is omitted)')
2129
- .option('--date <yyyy|yyyy-mm|yyyy-mm-dd>', 'sync only this date/month/year range')
2130
- .option('--max-pages <number>', 'max pages to fetch', '10')
2131
- .action(async (options) => {
2132
- await (0, credentials_1.ensureAccessToken)();
2133
- if (options.date && options.days) {
2134
- console.error('Error: --date and --days cannot be used together.');
2135
- process.exit(1);
2136
- }
2137
- const maxPages = parsePositiveIntegerOption(options.maxPages, '--max-pages');
2138
- let startDate;
2139
- let endDate;
2140
- if (options.date) {
2141
- const parsed = parseDateOption(options.date);
2142
- startDate = parsed.start;
2143
- endDate = parsed.end;
2144
- }
2145
- else {
2146
- const days = options.days ? parsePositiveIntegerOption(options.days, '--days') : 1;
2147
- const now = new Date();
2148
- endDate = new Date(now);
2149
- endDate.setDate(endDate.getDate() - 1);
2150
- endDate.setHours(23, 59, 59, 999);
2151
- startDate = new Date(now);
2152
- startDate.setDate(startDate.getDate() - days - 1);
2153
- startDate.setHours(0, 0, 0, 0);
2154
- }
2155
- console.log(`Syncing images between ${startDate.toISOString()} and ${endDate.toISOString()}...`);
2156
- const hourlyIndices = new Map();
2157
- for (let page = 1; page <= maxPages; page++) {
2158
- const images = await (0, api_1.listImages)(page, 100);
2159
- if (images.length === 0)
2160
- break;
2161
- let reachedLimit = false;
2162
- for (const img of images) {
2163
- const createdAt = new Date(img.created_at);
2164
- if (createdAt > endDate) {
2165
- // Skip images newer than target range.
2166
- continue;
2167
- }
2168
- if (createdAt < startDate) {
2169
- reachedLimit = true;
2170
- break;
2171
- }
2172
- // Add to hourly index
2173
- const y = createdAt.getFullYear().toString();
2174
- const m = (createdAt.getMonth() + 1).toString().padStart(2, '0');
2175
- const d = createdAt.getDate().toString().padStart(2, '0');
2176
- const h = createdAt.getHours().toString().padStart(2, '0');
2177
- const key = `${y}-${m}-${d}-${h}`;
2178
- if (!hourlyIndices.has(key))
2179
- hourlyIndices.set(key, new Set());
2180
- hourlyIndices.get(key)?.add(img.image_id);
2181
- const cached = (0, storage_1.loadImageCache)(img.image_id);
2182
- if (cached && cached.ocr) {
2183
- process.stdout.write(`s`);
2184
- continue;
2185
- }
2186
- process.stdout.write(`.`);
2187
- try {
2188
- const detail = await (0, api_1.getImageDetail)(img.image_id);
2189
- (0, storage_1.saveImageCache)(img.image_id, detail);
2190
- await new Promise(resolve => setTimeout(resolve, 200));
2191
- }
2192
- catch (e) {
2193
- process.stdout.write(`x`);
2194
- }
2195
- }
2196
- console.log(`\nPage ${page} processed.`);
2197
- if (reachedLimit)
2198
- break;
2199
- }
2200
- // Save hourly indices
2201
- console.log(`Updating hourly indices...`);
2202
- for (const [key, ids] of hourlyIndices.entries()) {
2203
- const [y, m, d, h] = key.split('-');
2204
- const existing = (0, storage_1.loadHourlyCache)(y, m, d, h) || [];
2205
- const merged = Array.from(new Set([...existing, ...ids]));
2206
- (0, storage_1.saveHourlyCache)(y, m, d, h, merged);
2207
- }
2208
- console.log(`Sync complete.`);
2209
- });
2210
- program
2211
- .command('import <type> <dir>')
2212
- .description('Import legacy data (type: json|hourly)')
2213
- .action(async (type, dir) => {
2214
- const sourceDir = path_1.default.resolve(dir);
2215
- if (!fs_1.default.existsSync(sourceDir)) {
2216
- console.error(`Error: Source directory ${sourceDir} does not exist.`);
2217
- process.exit(1);
2218
- }
2219
- if (type === 'json') {
2220
- const targetDir = path_1.default.join((0, storage_1.getCacheDir)(), 'images');
2221
- console.log(`Importing legacy Gyazo JSON from ${sourceDir}...`);
2222
- let total = 0;
2223
- const walk = (d) => {
2224
- fs_1.default.readdirSync(d, { withFileTypes: true }).forEach(e => {
2225
- const p = path_1.default.join(d, e.name);
2226
- if (e.isDirectory())
2227
- walk(p);
2228
- else if (e.name.endsWith('.json')) {
2229
- const id = e.name.replace('.json', '');
2230
- const p1 = id[0] || '_', p2 = id[1] || '_';
2231
- const dest = path_1.default.join(targetDir, p1, p2);
2232
- if (!fs_1.default.existsSync(dest))
2233
- fs_1.default.mkdirSync(dest, { recursive: true });
2234
- fs_1.default.copyFileSync(p, path_1.default.join(dest, e.name));
2235
- total++;
2236
- if (total % 100 === 0)
2237
- process.stdout.write('.');
2238
- }
2239
- });
2240
- };
2241
- walk(sourceDir);
2242
- console.log(`\nImport complete. Copied ${total} files.`);
2243
- }
2244
- else if (type === 'hourly') {
2245
- console.log(`Importing legacy Gyazo hourly data from ${sourceDir}...`);
2246
- let total = 0;
2247
- const years = fs_1.default.readdirSync(sourceDir).filter(f => /^[0-9]{4}$/.test(f));
2248
- for (const y of years) {
2249
- const months = fs_1.default.readdirSync(path_1.default.join(sourceDir, y)).filter(f => /^[0-9]{2}$/.test(f));
2250
- for (const m of months) {
2251
- const days = fs_1.default.readdirSync(path_1.default.join(sourceDir, y, m)).filter(f => /^[0-9]{2}$/.test(f));
2252
- for (const d of days) {
2253
- const hours = fs_1.default.readdirSync(path_1.default.join(sourceDir, y, m, d)).filter(f => /^[0-9]{2}$/.test(f));
2254
- for (const h of hours) {
2255
- const txt = path_1.default.join(sourceDir, y, m, d, h, 'image_ids.txt');
2256
- if (fs_1.default.existsSync(txt)) {
2257
- const ids = fs_1.default.readFileSync(txt, 'utf-8').split('\n').map(id => id.trim()).filter(id => id.length > 0);
2258
- (0, storage_1.saveHourlyCache)(y, m, d, h, ids);
2259
- total++;
2260
- }
2261
- }
2262
- }
2263
- }
2264
- process.stdout.write('.');
2265
- }
2266
- console.log(`\nImport complete. Copied ${total} hourly index files.`);
2267
- }
2268
- else {
2269
- console.error('Error: type must be "json" or "hourly"');
2270
- process.exit(1);
2271
- }
2272
- });
2273
- program.parseAsync(process.argv);