Skip to content

Use EXIF OffsetTimeOriginal for accurate saleStart, don't guess otherwise#401

Merged
techeng322 merged 2 commits into
mainfrom
techengme/telegram-exif-offset-time-original
Jul 9, 2026
Merged

Use EXIF OffsetTimeOriginal for accurate saleStart, don't guess otherwise#401
techeng322 merged 2 commits into
mainfrom
techengme/telegram-exif-offset-time-original

Conversation

@techeng322

@techeng322 techeng322 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Follow-up to Position Telegram bulk-upload moments by EXIF capture date #400. That PR treated EXIF DateTimeOriginal as UTC whenever timezone info was missing, which is itself an unverified guess (offset 0 is no more justified than any other offset).
  • When the camera also writes OffsetTimeOriginal (EXIF 2.31+), use it to compute the true UTC capture instant — this is verified data written by the device at capture time.
  • When OffsetTimeOriginal is absent, skip EXIF entirely (even if DateTimeOriginal is present) and fall back to upload time, same as having no capture-date signal at all. saleStart is written on-chain, so an unverified guess is worse there than an honest fallback.
  • Deliberately not falling back to a self-reported/profile-configured artist timezone either, for the same reason — only camera-verified offset data is trusted.

Test plan

  • pnpm vitest run src/lib/telegram — 41 files / 237 tests passing
  • pnpm lint clean on touched files
  • Verified against real sample photos: none of our HONOR-device samples populate OffsetTimeOriginal, so they correctly fall back to upload time (previously they were incorrectly treated as UTC, causing display glitches like the capture date appearing to roll to the next day for viewers in other timezones)

Summary by CodeRabbit

  • Bug Fixes

    • Improved photo capture date detection by using EXIF time offset information when available, resulting in more accurate timestamps.
    • Skips EXIF-based capture time when the offset is missing or invalid, helping avoid incorrect date guesses.
  • New Features

    • Added support for parsing EXIF-style time offsets in +HH:MM / -HH:MM format.
  • Tests

    • Expanded test coverage for offset parsing and capture-date handling, including positive and negative offsets and malformed input.

techeng322 and others added 2 commits July 10, 2026 01:58
…ovides it

DateTimeOriginal alone has no timezone, so we were assuming UTC for every
photo regardless of where it was actually taken. Some cameras/phones (EXIF
2.31+) also write OffsetTimeOriginal, which lets us compute the true UTC
capture instant instead of guessing.

Deliberately not falling back to a self-reported or profile-configured
timezone when OffsetTimeOriginal is absent: saleStart is written on-chain,
and an unverified guess is worse there than the existing, honest fallback
to upload time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous commit still fell back to treating DateTimeOriginal as UTC
when OffsetTimeOriginal was absent — but assuming a 0 offset is just as
much a guess as assuming any other timezone. Since saleStart is written
on-chain, skip EXIF entirely in that case and let the caller fall back
to upload time, consistent with having no capture-date signal at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
in-process-api Ready Ready Preview Jul 9, 2026 4:05pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new parseExifOffsetMs module to convert EXIF OffsetTimeOriginal strings into signed milliseconds, and updates extractExifCaptureDate to use that offset when converting DateTimeOriginal to a UTC epoch, returning undefined when no valid offset is available. Test suites are added/updated accordingly.

Changes

EXIF offset-aware capture date extraction

Layer / File(s) Summary
EXIF offset parser
src/lib/telegram/chat/parseExifOffsetMs.ts, src/lib/telegram/chat/__tests__/parseExifOffsetMs.test.ts
New default export parseExifOffsetMs parses ±HH:MM strings into signed milliseconds, returning undefined for non-string or malformed input; new test suite covers positive/negative offsets and invalid inputs.
Offset-adjusted capture date extraction
src/lib/telegram/chat/extractExifCaptureDate.ts
extractExifCaptureDate now picks OffsetTimeOriginal from EXIF, converts it via parseExifOffsetMs, returns undefined if unavailable, and computes the UTC instant as (localAsUtcMs - offsetMs) / 1000 instead of assuming DateTimeOriginal is already UTC.
extractExifCaptureDate test updates
src/lib/telegram/chat/__tests__/extractExifCaptureDate.test.ts
Tests updated to assert exifr.parse requests both fields with reviveValues: false, verify correct UTC conversion for positive and negative offsets, and check undefined returns for missing/malformed offsets or invalid DateTimeOriginal.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: using OffsetTimeOriginal for saleStart and avoiding timezone guessing when it is absent.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch techengme/telegram-exif-offset-time-original

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/lib/telegram/chat/__tests__/extractExifCaptureDate.test.ts

Oops! Something went wrong! :(

ESLint: 9.39.4

TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'configs' -> object with constructor 'Object'
| property 'flat' -> object with constructor 'Object'
| ...
| property 'plugins' -> object with constructor 'Object'
--- property 'react' closes the circle
Referenced from: /.eslintrc.json
at JSON.stringify ()
at /node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2255:45
at Array.map ()
at ConfigValidator.formatErrors (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2246:23)
at ConfigValidator.validateConfigSchema (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2277:84)
at ConfigArrayFactory._normalizeConfigData (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/

... [truncated 446 characters] ...

c/dist/eslintrc.cjs:3261:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3199:25)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)

src/lib/telegram/chat/__tests__/parseExifOffsetMs.test.ts

Oops! Something went wrong! :(

ESLint: 9.39.4

TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'configs' -> object with constructor 'Object'
| property 'flat' -> object with constructor 'Object'
| ...
| property 'plugins' -> object with constructor 'Object'
--- property 'react' closes the circle
Referenced from: /.eslintrc.json
at JSON.stringify ()
at /node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2255:45
at Array.map ()
at ConfigValidator.formatErrors (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2246:23)
at ConfigValidator.validateConfigSchema (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2277:84)
at ConfigArrayFactory._normalizeConfigData (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/

... [truncated 446 characters] ...

c/dist/eslintrc.cjs:3261:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3199:25)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)

src/lib/telegram/chat/extractExifCaptureDate.ts

Oops! Something went wrong! :(

ESLint: 9.39.4

TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'configs' -> object with constructor 'Object'
| property 'flat' -> object with constructor 'Object'
| ...
| property 'plugins' -> object with constructor 'Object'
--- property 'react' closes the circle
Referenced from: /.eslintrc.json
at JSON.stringify ()
at /node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2255:45
at Array.map ()
at ConfigValidator.formatErrors (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2246:23)
at ConfigValidator.validateConfigSchema (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2277:84)
at ConfigArrayFactory._normalizeConfigData (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/

... [truncated 446 characters] ...

c/dist/eslintrc.cjs:3261:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/.pnpm/@eslint+eslintrc@3.3.5/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3199:25)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)

  • 1 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
src/lib/telegram/chat/__tests__/extractExifCaptureDate.test.ts (1)

31-55: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a test case for +00:00 (UTC) offset.

The suite covers positive (+09:00) and negative (-05:00) offsets but not the UTC zero-offset case. A +00:00 offset should produce a valid timestamp (equal to Date.UTC(...) / 1000), not undefined. This would catch a regression where 0 is mistakenly treated as a missing offset at the integration level.

✅ Suggested test addition
   it('subtracts a negative OffsetTimeOriginal correctly', async () => {
     mockParse.mockResolvedValue({
       DateTimeOriginal: '2026:07:09 23:32:10',
       OffsetTimeOriginal: '-05:00',
     });

     const result = await extractExifCaptureDate(Buffer.from('jpeg-bytes'));

     expect(result).toBe(
       (Date.UTC(2026, 6, 9, 23, 32, 10) + 5 * 60 * 60_000) / 1000
     );
   });
+
+  it('returns a valid timestamp for +00:00 (UTC) offset', async () => {
+    mockParse.mockResolvedValue({
+      DateTimeOriginal: '2026:07:09 23:32:10',
+      OffsetTimeOriginal: '+00:00',
+    });
+
+    const result = await extractExifCaptureDate(Buffer.from('jpeg-bytes'));
+
+    expect(result).toBe(Date.UTC(2026, 6, 9, 23, 32, 10) / 1000);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/telegram/chat/__tests__/extractExifCaptureDate.test.ts` around lines
31 - 55, Add a test in extractExifCaptureDate.test.ts for the
extractExifCaptureDate path when parse returns DateTimeOriginal with
OffsetTimeOriginal set to +00:00. Assert the result equals Date.UTC(...) / 1000
and is not undefined, so the UTC-zero case is covered alongside the existing
positive and negative offset tests.
src/lib/telegram/chat/extractExifCaptureDate.ts (1)

2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use @/* path alias instead of relative import.

Per coding guidelines, imports pointing to ./src/* should use the @/* alias.

♻️ Suggested change
-import parseExifOffsetMs from './parseExifOffsetMs';
+import parseExifOffsetMs from '`@/lib/telegram/chat/parseExifOffsetMs`';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/telegram/chat/extractExifCaptureDate.ts` at line 2, The import in
extractExifCaptureDate should use the project path alias instead of a relative
reference. Update the parseExifOffsetMs import to follow the `@/`* convention used
by the codebase, keeping the symbol name the same while changing only the import
path style.

Source: Coding guidelines

src/lib/telegram/chat/__tests__/parseExifOffsetMs.test.ts (2)

2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use @/* path alias instead of relative import.

Per coding guidelines, imports pointing to ./src/* should use the @/* alias.

♻️ Suggested change
-import parseExifOffsetMs from '../parseExifOffsetMs';
+import parseExifOffsetMs from '`@/lib/telegram/chat/parseExifOffsetMs`';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/telegram/chat/__tests__/parseExifOffsetMs.test.ts` at line 2, The
test file currently imports parseExifOffsetMs using a relative path; update the
import in parseExifOffsetMs.test.ts to use the `@/`* alias instead of
../parseExifOffsetMs. Keep the same symbol name, and make sure all imports in
this area follow the alias-based path convention used by the project.

Source: Coding guidelines


4-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a test case for +00:00 (UTC offset).

The suite doesn't cover the +00:00 case, which returns 0 — a valid offset distinct from undefined. The downstream consumer (extractExifCaptureDate.ts) correctly checks offsetMs === undefined rather than a falsy check, but a test here would guard against a future regression where someone switches to !offsetMs and silently breaks UTC photos.

✅ Suggested test addition
   it('returns undefined for a non-string value', () => {
     expect(parseExifOffsetMs(540)).toBeUndefined();
   });
+
+  it('returns 0 for a +00:00 (UTC) offset, not undefined', () => {
+    expect(parseExifOffsetMs('+00:00')).toBe(0);
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/telegram/chat/__tests__/parseExifOffsetMs.test.ts` around lines 4 -
28, Add a test in parseExifOffsetMs.test.ts to cover the UTC case `+00:00`,
since `parseExifOffsetMs` should return `0` and not `undefined`. Update the
existing `parseExifOffsetMs` test suite by adding a case alongside the other
offset parsing checks, and make sure it explicitly asserts zero is returned for
`+00:00` to protect callers like `extractExifCaptureDate` from regressions that
treat `0` as falsy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/lib/telegram/chat/__tests__/extractExifCaptureDate.test.ts`:
- Around line 31-55: Add a test in extractExifCaptureDate.test.ts for the
extractExifCaptureDate path when parse returns DateTimeOriginal with
OffsetTimeOriginal set to +00:00. Assert the result equals Date.UTC(...) / 1000
and is not undefined, so the UTC-zero case is covered alongside the existing
positive and negative offset tests.

In `@src/lib/telegram/chat/__tests__/parseExifOffsetMs.test.ts`:
- Line 2: The test file currently imports parseExifOffsetMs using a relative
path; update the import in parseExifOffsetMs.test.ts to use the `@/`* alias
instead of ../parseExifOffsetMs. Keep the same symbol name, and make sure all
imports in this area follow the alias-based path convention used by the project.
- Around line 4-28: Add a test in parseExifOffsetMs.test.ts to cover the UTC
case `+00:00`, since `parseExifOffsetMs` should return `0` and not `undefined`.
Update the existing `parseExifOffsetMs` test suite by adding a case alongside
the other offset parsing checks, and make sure it explicitly asserts zero is
returned for `+00:00` to protect callers like `extractExifCaptureDate` from
regressions that treat `0` as falsy.

In `@src/lib/telegram/chat/extractExifCaptureDate.ts`:
- Line 2: The import in extractExifCaptureDate should use the project path alias
instead of a relative reference. Update the parseExifOffsetMs import to follow
the `@/`* convention used by the codebase, keeping the symbol name the same while
changing only the import path style.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 584a9e4e-3928-42a3-a0db-541a500b4d87

📥 Commits

Reviewing files that changed from the base of the PR and between 7b00e69 and 4818007.

📒 Files selected for processing (4)
  • src/lib/telegram/chat/__tests__/extractExifCaptureDate.test.ts
  • src/lib/telegram/chat/__tests__/parseExifOffsetMs.test.ts
  • src/lib/telegram/chat/extractExifCaptureDate.ts
  • src/lib/telegram/chat/parseExifOffsetMs.ts

@techeng322 techeng322 merged commit 8b3cc3c into main Jul 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant