Skip to content

Instantly share code, notes, and snippets.

@ahbanavi
Created August 6, 2026 14:17
Show Gist options
  • Select an option

  • Save ahbanavi/b937a25df7846c6b4ff49071e44f408f to your computer and use it in GitHub Desktop.

Select an option

Save ahbanavi/b937a25df7846c6b4ff49071e44f408f to your computer and use it in GitHub Desktop.
Jellyfin on LG webOS: every clock off by your UTC offset — webOS's WebAppManager engine resolves Intl's default time zone to Etc/Unknown (= UTC) while Date is correct. Full diagnosis + a self-gating JS fix.

Jellyfin on LG webOS: every clock is off by exactly your UTC offset

TL;DR — webOS's WebAppManager engine resolves the default Intl time zone to Etc/Unknown, which ICU formats as UTC. Date is perfectly correct; only Intl is broken. jellyfin-web renders every clock through Intl, so they all shift together by your UTC offset. Nothing about your TV, server, or container time zone is misconfigured. Fix: pin the zone from JavaScript (webos-intl-timezone-fix.js).


Symptoms

In the Jellyfin app on an LG Smart TV, every rendered time is behind (or ahead) by exactly your UTC offset:

  • the header clock next to the profile image
  • "Ends at HH:MM" on media detail pages
  • "Ends at HH:MM" in the playback info / OSD

Observed with a UTC+03:30 device: real time 17:10, Jellyfin showed 13:40.

Everything else about the app works normally. Other Jellyfin clients on the same server (desktop browser, phone, Android TV) show the correct time.

What is not the cause

Worth stating plainly, because these are the obvious suspects and all of them are dead ends:

  • ❌ The TV's time zone setting. It is correct, and setting it again changes nothing.
  • ❌ The Jellyfin server's TZ env var or the host's /etc/timezone.
  • ❌ Jellyfin's user display preferences.
  • ❌ A stale app process holding an old TZ. Force-closing and relaunching the app does not help.
  • ❌ Anything server-side at all. The server is only serving UTC timestamps over the API, which is correct and normal — clients are supposed to localize them.

The trap that makes this hard to diagnose

Do not diagnose this using the TV's Web Browser app. webOS runs two different web engines:

App User-Agent Intl default zone toLocaleTimeString()
Web Browser ...(Linux; NetCast; U)... Colt/2.0 undefined ✅ correct local time
Jellyfin (and other web apps) ...(Web0S; Linux/SmartTV)... WebAppManager Etc/Unknown ❌ UTC

Point the TV's browser at a page that prints new Date() and the TV looks completely healthy. The bug only reproduces inside a webOS app.

Evidence

Captured from inside the running Jellyfin app on an LG C5 (webOS, Chrome 120 / WebAppManager), device zone UTC+03:30:

{
  "toString":        "Thu Aug 06 2026 17:35:52 GMT+0330 (+03:30)",
  "offsetMin":       -210,
  "getHours":        17,
  "bare_localeTime": "2:05:52 PM",
  "opt_localeTime":  "2:05 PM",
  "tz_UTC":          "2:05 PM",
  "tz_Tehran":       "5:35 PM",
  "intlTZ":          "Etc/Unknown",
  "intlLocale":      "en-US",
  "ua":              "Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.270 Safari/537.36 WebAppManager"
}

Reading that:

  • toString, offsetMin and getHours are all correct — the engine knows it is UTC+03:30.
  • bare_localeTime is identical to tz_UTC — proof that the default zone is being treated as UTC.
  • tz_Tehran (an explicitly passed IANA zone) is correct — so ICU has the zone data; it just can't resolve the default.
  • intlTZ is Etc/Unknown — the root cause. It is ICU's "I have no idea" sentinel, and it behaves as UTC.

Root cause

The webOS Jellyfin client (jellyfin-webos) is a thin wrapper. Its README:

This is a small wrapper around the web interface provided by the server … the app hands off control to the hosted webUI.

So the code running on your TV is your server's own jellyfin-web, executing under webOS's WebAppManager engine. That engine fails to hand ICU a usable default time zone, so Intl.DateTimeFormat().resolvedOptions().timeZone comes back as Etc/Unknown and every Intl format call without an explicit timeZone renders in UTC.

jellyfin-web formats all of its clocks through Date.prototype.toLocaleTimeString() / toLocaleString() (see datetime.js), which are Intl-backed. Hence: every clock, all shifted by the same amount, while the underlying Date objects are correct.

Because it is a wrapper, the fix can be injected from the server — no need to modify, sideload, or root anything on the TV.

The fix

webos-intl-timezone-fix.js wraps Intl.DateTimeFormat and the three Date.prototype.toLocale*String methods so that a missing timeZone option defaults to your zone.

Set TIME_ZONE at the top of the file to your IANA zone (Europe/Berlin, America/New_York, Asia/Tehran, …). It cannot be auto-detected — the engine's inability to report it is the bug.

It is safe to apply server-wide because it gates twice before patching anything:

  1. the engine's own Intl formatting must disagree with Date.prototype.getHours(), and
  2. your configured zone must match the device's real local time.

On a healthy client both gates fail and the script returns immediately, leaving Intl untouched. Verified: on desktop Chrome/Edge the marker window.__webosIntlTimeZonePatched stays undefined and formatting is unchanged.

It is also idempotent (guarded by that same marker), and it deliberately preserves RangeError on invalid locales, because jellyfin-web's datetime.js feature-detects localization support with toLocaleTimeString('i') and expects it to throw.

Install — JavaScript Injector plugin (recommended)

  1. Install the JavaScript Injector plugin (Dashboard ▸ Plugins).
  2. Dashboard ▸ Plugins ▸ JavaScript Injector ▸ add a custom script.
  3. Paste the file contents, set Enabled, leave Requires Authentication off so it loads before login too.
  4. Save.

Takes effect on the client's next load — no Jellyfin restart required. Relaunch the Jellyfin app on the TV.

Install — same plugin, via the API

Useful for scripting it. The plugin stores scripts in its plugin configuration, so read-modify-write:

JELLYFIN_URL="http://your-server:8096"
API_KEY="your-api-key"                              # Dashboard > API Keys
PLUGIN_ID="f5a34f7b2e8a4e6aa7223a216a81b374"        # JavaScript Injector
AUTH="Authorization: MediaBrowser Token=${API_KEY}"

curl -fsS -H "$AUTH" "${JELLYFIN_URL}/Plugins/${PLUGIN_ID}/Configuration" -o config.json

python3 - <<'PY'
import json
config = json.load(open('config.json'))
source = open('webos-intl-timezone-fix.js', encoding='utf-8').read()
entries = config.setdefault('CustomJavaScripts', [])
name = 'webOS Intl timezone fix'
entry = next((e for e in entries if e.get('Name') == name), None)
if entry is None:
    entry = {'Name': name, 'Id': 'webos-intl-timezone-fix'}
    entries.append(entry)
entry.update({'Script': source, 'Enabled': True, 'RequiresAuthentication': False})
json.dump(config, open('config.json', 'w'))
PY

curl -fsS -X POST -H "$AUTH" -H "Content-Type: application/json" \
     --data-binary @config.json "${JELLYFIN_URL}/Plugins/${PLUGIN_ID}/Configuration"

Read-modify-write matters: a blind POST would drop entries other plugins have registered there.

Install — without the plugin

Add to jellyfin-web's index.html before </head>:

<script src="/web/webos-intl-timezone-fix.js"></script>

⚠️ This is wiped by every Jellyfin update, since it edits the shipped web root. The plugin route survives updates. (Worth a glance at the clock after a major upgrade either way.)

Verify

Confirm the script is actually being served:

curl -s "http://your-server:8096/JavaScriptInjector/public.js" | grep -c webosIntlTimeZonePatched

Then relaunch the Jellyfin app on the TV and look at the header clock.

To check programmatically, temporarily append this to the script — it prints to the app's console, or you can beacon it to a machine you control via an image request:

setTimeout(function () {
  var d = new Date();
  console.log(JSON.stringify({
    toString: d.toString(),
    getHours: d.getHours(),
    localeTime: d.toLocaleTimeString(),
    intlTZ: Intl.DateTimeFormat().resolvedOptions().timeZone,
    patched: window.__webosIntlTimeZonePatched
  }));
}, 5000);

Before → after on the affected TV:

before after
toLocaleTimeString() 2:05:52 PM 5:40:12 PM
Intl…resolvedOptions().timeZone Etc/Unknown Asia/Tehran
__webosIntlTimeZonePatched undefined Asia/Tehran

Notes

  • Scope. This patches display formatting only. Date objects, the epoch values sent to the server, and playback reporting are untouched — nothing is shifted on the wire.
  • DST. Pinning a named IANA zone (not a fixed offset) means DST is handled by ICU as normal.
  • Other webOS web apps built the same way are likely affected by the same engine bug; the same wrapper approach applies.
  • Applies to Tizen (Samsung) too? Unverified here. If your Samsung client shows the same skew, check Intl.DateTimeFormat().resolvedOptions().timeZone first — if it reports Etc/Unknown, this fix should apply unchanged.
  • Tested on an LG C5 (OLED65C56LA), webOS WebAppManager / Chrome 120, Jellyfin server 12.0.0, Jellyfin for WebOS 1.2.2, JavaScript Injector 3.4.0.

License

Public domain / CC0. Use it however you like.

/**
* Fix clocks in the Jellyfin webOS (LG Smart TV) client showing the wrong time.
*
* PROBLEM
* Every clock jellyfin-web renders is off by exactly your UTC offset — the
* header clock next to the profile image, every "Ends at HH:MM" on media
* detail pages and in playback info. The TV's own time zone setting is
* correct, and so are the Jellyfin server's host and container time zones.
*
* CAUSE
* webOS's WebAppManager engine resolves the default Intl time zone to
* "Etc/Unknown", which ICU formats as UTC:
*
* new Date().toString() -> "... 17:35:52 GMT+0330 (+03:30)" correct
* new Date().getHours() -> 17 correct
* new Date().toLocaleTimeString() -> "2:05:52 PM" UTC
* Intl.DateTimeFormat().resolvedOptions().timeZone -> "Etc/Unknown"
*
* Date is fine; only Intl-based formatting is wrong. jellyfin-web renders
* every clock through Intl, so they all shift together.
*
* FIX
* Passing an explicit IANA zone formats correctly, so this pins the default
* zone by wrapping Intl.DateTimeFormat and the three Date.prototype
* toLocale*String methods.
*
* SAFETY
* Self-gating: it patches nothing unless the engine's own formatting
* disagrees with getHours() AND the configured zone matches the device's
* real local time. Healthy clients (desktop, phone, Android TV) are
* untouched, so it is safe to apply server-wide.
*
* SETUP
* Set TIME_ZONE below to your IANA zone, e.g. "Europe/Berlin",
* "America/New_York", "Asia/Tehran". It cannot be auto-detected — the
* engine's inability to report it is the bug being worked around.
*/
(function () {
'use strict';
// ---- CHANGE THIS to your IANA time zone -------------------------------
var TIME_ZONE = 'Asia/Tehran';
// -----------------------------------------------------------------------
if (window.__webosIntlTimeZonePatched) {
return;
}
var NativeDateTimeFormat = Intl.DateTimeFormat;
var nativeToLocaleString = Date.prototype.toLocaleString;
var nativeToLocaleTimeString = Date.prototype.toLocaleTimeString;
var nativeToLocaleDateString = Date.prototype.toLocaleDateString;
/** Hour that Intl renders for `date`, in `zone` (omit zone for the engine default). */
function formattedHour(date, zone) {
var options = { hour: '2-digit', hour12: false };
if (zone) {
options.timeZone = zone;
}
var hour = parseInt(new NativeDateTimeFormat('en-GB', options).format(date), 10);
return hour === 24 ? 0 : hour; // en-GB renders midnight as "24"
}
var now = new Date();
try {
// Healthy engine: Intl's default zone already agrees with the clock. Nothing to do.
if (formattedHour(now) === now.getHours()) {
return;
}
// Only pin a zone that actually matches this device's real local time, so a
// client in some other zone is never forced onto TIME_ZONE.
if (formattedHour(now, TIME_ZONE) !== now.getHours()) {
return;
}
} catch (err) {
return; // Can't tell what the engine is doing — leave it alone.
}
function withZone(options) {
var merged = {};
if (options) {
for (var key in options) {
if (Object.prototype.hasOwnProperty.call(options, key)) {
merged[key] = options[key];
}
}
}
if (!merged.timeZone) {
merged.timeZone = TIME_ZONE;
}
return merged;
}
function PatchedDateTimeFormat(locales, options) {
return new NativeDateTimeFormat(locales, withZone(options));
}
PatchedDateTimeFormat.prototype = NativeDateTimeFormat.prototype;
PatchedDateTimeFormat.supportedLocalesOf = function (locales, options) {
return NativeDateTimeFormat.supportedLocalesOf(locales, options);
};
Intl.DateTimeFormat = PatchedDateTimeFormat;
// Invalid locales must still throw RangeError here — jellyfin-web's
// datetime.js feature-detects localization support with toLocaleTimeString('i').
Date.prototype.toLocaleString = function (locales, options) {
return nativeToLocaleString.call(this, locales, withZone(options));
};
Date.prototype.toLocaleTimeString = function (locales, options) {
return nativeToLocaleTimeString.call(this, locales, withZone(options));
};
Date.prototype.toLocaleDateString = function (locales, options) {
return nativeToLocaleDateString.call(this, locales, withZone(options));
};
window.__webosIntlTimeZonePatched = TIME_ZONE;
console.log('[webos-tz] Intl default zone was unusable; pinned formatting to ' + TIME_ZONE);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment