Skip to content

fix(api): send YouTube's utcOffsetMinutes sign, fixing YTMusicClient - #423

Open
YuriNachos wants to merge 1 commit into
sozercan:mainfrom
YuriNachos:YuriNachos/w7-kaset
Open

fix(api): send YouTube's utcOffsetMinutes sign, fixing YTMusicClient#423
YuriNachos wants to merge 1 commit into
sozercan:mainfrom
YuriNachos:YuriNachos/w7-kaset

Conversation

@YuriNachos

@YuriNachos YuriNachos commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Description

YTMusicClient negates TimeZone.secondsFromGMT() / 60 before putting it into the InnerTube client context, so every non-UTC user sends an inverted utcOffsetMinutes (Los Angeles goes out as +420 instead of -420).

This PR removes the negation, routes all three call sites through one tested helper, and pins the convention with unit tests.

Note on the earlier revision of this PR: it argued the opposite — that utcOffsetMinutes follows the JavaScript getTimezoneOffset() convention and that the two unnegated call sites were wrong. @sozercan caught that this was backwards, and he is right. The branch has been rewritten accordingly; details under Root cause below.

AI Prompt

Assisted with Claude Code. The prompt for the revision was, in substance: "The maintainer's review says the sign convention is inverted. Verify against an external reference implementation of the InnerTube context rather than against our own reasoning; if the maintainer is right, invert the change, make YTMusicClient the fixed call site, reverse the test expectations, and add a case pinning the Los Angeles datum from the review discussion. Prove the tests are non-vacuous by reintroducing the negation and showing them go red."

Root cause

Three facts, each verifiable:

  1. Date.prototype.getTimezoneOffset() returns UTC − local, so Los Angeles yields +420.

  2. YouTube's web client sends -Date.getTimezoneOffset(), i.e. -420 for Los Angeles. In YouTube.js, src/core/Session.ts:

    utcOffsetMinutes: -Math.floor((new Date()).getTimezoneOffset()),
  3. Foundation's TimeZone.secondsFromGMT() already carries the opposite sign from getTimezoneOffset():

    zone secondsFromGMT() / 60
    America/Los_Angeles -420
    America/New_York -240
    Asia/Kolkata +330
    Europe/Berlin +120
    UTC 0

So secondsFromGMT() / 60 is already the value YouTube expects, and no negation belongs anywhere. YouTubeClient and AskVideoAudit were correct; YTMusicClient was the one genuine bug.

As a cross-check, yt-dlp and NewPipeExtractor both sidestep the question entirely by pinning timeZone: "UTC" with utcOffsetMinutes: 0, which is consistent with the sign only mattering when the value is non-zero.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Related Issues

None — self-found while reading the InnerTube context builders.

Changes Made

  • InnerTubeSupport.utcOffsetMinutes(for:) — new shared helper returning timeZone.secondsFromGMT() / 60, with a doc comment stating the convention and why there is no negation.
  • YTMusicClient.swift:1962the actual fix: dropped the negation by routing through the helper.
  • YouTubeClient.swift:692 — routed through the helper; emitted value unchanged.
  • AskVideoAudit.swift:406 — unchanged value, with a comment explaining why it is inlined (APIExplorer cannot import the Kaset executable target).
  • InnerTubeSupportTests.swift — 5 cases covering UTC-behind, UTC-ahead, UTC, the half-hour India offset, and the Los Angeles reference datum. All use TimeZone(secondsFromGMT:) so DST cannot move an assertion.

Testing

  • swift build — clean
  • swift test --skip KasetUITests — 2958 tests, 231 suites, passing
  • swiftlint --strict — 0 violations in 615 files
  • swiftformat --lint . — 0 files require formatting
  • macOS 26

Non-vacuity check: reintroducing the negation in the helper turns 4 of the 9 InnerTubeSupport tests red; restoring it makes them green again. The tests genuinely constrain the sign rather than merely describing it.

Checklist

  • Code follows the project style
  • Lint is clean
  • Tests pass
  • No new warnings
  • Docs not applicable (no user-facing behaviour documented for this field)

@sozercan sozercan left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Could we double-check the sign convention here? YouTube’s current WEB and Music clients appear to send -Date.getTimezoneOffset(). Since Foundation’s secondsFromGMT() already has the opposite sign from getTimezoneOffset(), wouldn’t secondsFromGMT() / 60 be the correct equivalent?

For example, Los Angeles yields JS +420, while YouTube sends -420 and Foundation returns -420. Would the added negation therefore incorrectly send +420? If so, might YTMusicClient be the existing incorrect call site, with these new test expectations needing reversal?

`YTMusicClient` negated `TimeZone.secondsFromGMT() / 60` before putting it in
the InnerTube `client` context, so every non-UTC user sent an inverted offset
(Los Angeles went out as `+420` instead of `-420`).

YouTube's own web client computes `-Date.getTimezoneOffset()`, and Foundation's
`secondsFromGMT()` already carries the opposite sign from `getTimezoneOffset()`,
so `secondsFromGMT() / 60` is already the value YouTube expects and no negation
belongs anywhere.

Route all three call sites through one tested
`InnerTubeSupport.utcOffsetMinutes(for:)` helper and pin the convention with
unit tests, including the Los Angeles reference datum.

Co-authored-by: Claude <noreply@anthropic.com>
@YuriNachos
YuriNachos force-pushed the YuriNachos/w7-kaset branch from 380fa3b to e87b7d5 Compare August 9, 2026 01:09
@YuriNachos YuriNachos changed the title fix: correct utcOffsetMinutes sign to match JS getTimezoneOffset convention fix(api): send YouTube's utcOffsetMinutes sign, fixing YTMusicClient Aug 9, 2026
@YuriNachos

Copy link
Copy Markdown
Contributor Author

You are right, and my original premise was backwards. Thank you for catching it — I went and checked against an external reference rather than re-reasoning from the same assumption.

The reference implementation agrees with you. YouTube.js src/core/Session.ts builds the context as:

utcOffsetMinutes: -Math.floor((new Date()).getTimezoneOffset()),

So for Los Angeles, YouTube sends -420. And Foundation already has that sign:

zone secondsFromGMT() / 60 JS getTimezoneOffset() what YouTube sends
America/Los_Angeles -420 +420 -420
America/New_York -240 +240 -240
Asia/Kolkata +330 -330 +330
Europe/Berlin +120 -120 +120

Exactly as you said: secondsFromGMT() / 60 is already the correct equivalent, my negation would have sent +420, and YTMusicClient is the incorrect call site. (As a side check, yt-dlp and NewPipeExtractor both dodge this by pinning timeZone: "UTC" with utcOffsetMinutes: 0 — consistent with the sign only mattering when the value is non-zero.)

What the PR does now, rewritten as one commit (e87b7d5):

  • InnerTubeSupport.utcOffsetMinutes(for:) returns timeZone.secondsFromGMT() / 60, no negation.
  • YTMusicClient.swift:1962 routes through it — this is now the actual fix.
  • YouTubeClient and AskVideoAudit keep their original emitted values.
  • The four test expectations are reversed and renamed, plus a fifth case pinning the Los Angeles datum from your example.

I kept the shared helper since it was the part you did not object to, and I squashed the branch so the false premise is not sitting in the history.

Gate on the rewritten branch: swift build clean, swift test --skip KasetUITests 2958 tests passing, swiftlint --strict 0 violations in 615 files, swiftformat --lint . 0 files needing formatting. Non-vacuity: putting the negation back turns 4 of the 9 InnerTubeSupport tests red.

One unrelated thing I noticed while running the suite locally, in case it is useful: the full run is nondeterministic on this machine. Three consecutive swift test --skip KasetUITests runs on identical code gave fail / pass / pass, and the failing suite (PlaylistDetailViewModelTests) passes 57/57 when run in isolation. The same rotating failures show up in CI across unrelated branches, including your own refresh-home-suggestions. I wrote up the details on #421.

@YuriNachos

Copy link
Copy Markdown
Contributor Author

Addressed in e87b7d5696. The negation is gone: YTMusicClient now uses InnerTubeSupport.utcOffsetMinutes(for:) (= TimeZone.secondsFromGMT() / 60), so Los Angeles sends -420 — exactly what YouTube's web client sends (-Date.getTimezoneOffset()), matching the table above. YouTubeClient (which was already correct) is refactored to the same helper for a single source of truth. Could you re-review?

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.

2 participants