Почему телегид показывает неправильное время: метки времени и смещения в XMLTV

Если гид уехал на целое число часов, никакой загадки тут почти никогда нет. Это суффикс, который в файле был, а учтён не был.

<programme start="20260719150000 +0200" stop="20260719160000 +0200" channel="one.example.com">

Четырнадцать цифр, пробел и ещё пять символов, от которых зависит, начнётся эта строка в 15:00 или в 13:00. Четырнадцать цифр это не момент времени. Это показание настенных часов без привязки к месту, и правильный телегид держится на том, что последние пять символов будут прочитаны, а если их нет, то их отсутствие будет истолковано так, как имел в виду граббер, записавший файл.

Прежде чем что-то менять, измерьте сдвиг. Если все программы на всех каналах уехали на одно и то же целое число часов, сломано что-то одно, и почти всегда это смещение, которое в файле было, а учтено не было. Если часть каналов показывает верно, а часть нет, или одна строка уехала, а соседние в порядке, то это другая поломка, и ничто из написанного ниже про смещения её не касается.

Наш плеер разбирает телегид у вас на устройстве, по тому адресу, который вы указали. Мы курируем адреса телегидов, и над курируемыми на нашей стороне работает тот же самый парсер, для каталога. Адрес телегида, который дали вы, скачивается и читается на вашей машине, и данные вашего телегида мы у себя не храним.

Сдвиг всего телегида и одна неправильная программа это разные баги

У равномерного сдвига три правдоподобные причины, и различить их можно за минуту.

Первая: смещение в файле было, а считыватель его выбросил. Вторая: смещения в файле не было вовсе, граббер записал местное время по настенным часам без суффикса, и узнать об этом считывателю было неоткуда. Третья: поправку применили дважды, один раз источник и один раз вы, и именно за неё хватаются первым делом, и именно она даёт сдвиг ровно вдвое больше выставленного. Если после того, как вы выставили поправку в два часа, телегид уехал с одного часа на три, это третий случай, и лечится он тем, что поправку возвращают в ноль, прежде чем диагностировать что-либо ещё.

Измерение это два сравнения и часы. Возьмите программу, которая идёт прямо сейчас, и посмотрите, насколько заявленное начало расходится с реальностью, потом повторите на втором канале того же источника, потом на канале другого источника. Одинаковая ошибка на обоих каналах одного источника и никакой ошибки на другом источнике сужает дело до файла этого источника. Одинаковая ошибка везде указывает уже на часы устройства, а это совсем другой ремонт.

Неравномерный сдвиг это вообще не проблема часового пояса. Обычная причина в том, что строка телегида привязана не к тому каналу, и тогда правильное расписание какой-то чужой станции выглядит как неправильное расписание вашей. Это сбой идентификации, и как он устроен, разбирает отдельная статья. Наличие идентификатора в плейлисте и идентификатор, который совпадает с вашим телегидом, это не одно и то же: в большем из двух измеренных нами плейлистов 527 строк из 1895 в итоге остались вообще без пригодного идентификатора. Остальные неравномерные причины касаются длительности, а не времени начала, и о них ниже.

Суффикс, который начинается со знака, уже сказал вам, что он число

Вот правило, которое применяет наш считыватель меток времени, и комментарий над ним, дословно:

var suffix = value[14..].Trim();
if (suffix.Length > 0 && (suffix[0] == '+' || suffix[0] == '-'))
{
    // A suffix that looks like a numeric offset must read as one. Falling
    // through to the assumed timezone here is what used to shift a whole
    // feed by hours with nothing in the logs: the offset was present, said
    // +02:00, and was read as UTC.
    if (!TryParseOffset(suffix, out var offset))
    {
        return false;
    }

Этот комментарий описывает нашу собственную ошибку. В более ранней версии считывателя мы сделали это неправильно: нечитаемое смещение он обрабатывал ровно так же, как полное отсутствие смещения, то есть проваливался дальше, считал время за UTC и продолжал работать. Симптом: телегид смещён ровно на ту величину, которую файл сам и указывал, на каждом канале, на каждый день фида, при чистой сводке импорта и пустом логе. Причина: один недостающий ранний return. Цена: изнутри приложения этот сбой было не обнаружить, ни один счётчик не двигался, ни одной строки в лог не писалось, и телегид оставался неправильным, пока кто-нибудь не сверял его с часами.

Отказаться от строки хуже для этой одной программы и намного лучше для телегида. Отброшенная программа оставляет дырку, которую видно. Молча переосмысленный фид это расписание, которое уверенно и одинаково неправильно, и сколько на него ни смотри, оно вам об этом не скажет.

В этом правиле есть дыра, и лучше сказать о ней прямо. Проверка смотрит только на первый символ суффикса. Суффикс вида GMT+0200, где метка стоит перед знаком, а не после него, со знака не начинается, а значит уходит по тихой ветке. Прогон через выпущенный парсер возвращает 15:00 UTC для местных 15:00, то есть смещение лежит прямо в строке, и его игнорируют. Тестами эта форма не зафиксирована, а собственного корпуса XMLTV у нас нет, так что отсутствие свидетеля не доказывает, что она редкая.

Три формы смещения читаются, семь отвергаются

После знака считыватель берёт всё до первого пробела и меряет длину цепочки цифр. Осмысленных длин всего три: два символа для смещения в одних часах, четыре для часов и минут подряд и пять, когда третий символ это двоеточие. +02, +0200 и +02:00 дают одни и те же два часа. Всё, что короче трёх символов целиком, отсеивается ещё до switch, а любая другая длина вываливается в ветку default.

Все три формы это ISO 8601, потому все три и читаются. +0200 базовая форма, +02:00 расширенная, +02 только часы, а собственный пример XMLTV DTD записан в базовой форме, как +0300. Благословить одну форму и молча принять две другие за UTC значит увести на час и больше каждый фид, который выбрал иначе.

Вокруг этого есть две поблажки. Пробел перед знаком необязателен, потому что суффиксом считается всё начиная с пятнадцатого символа, так что 20260719150000+0200 работает точно так же. И метка пояса после настоящего смещения не считается повреждением, а просто отбрасывается: +0100 BST обрезается по пробелу, читается как +0100 и даёт 14:00 UTC для местных 15:00. Получасовые пояса получаются даром, потому что минуты разбираются как отдельные две цифры, так что +05:30 читается правильно.

У этой границы на пятнадцатом символе есть последствие, которого никто не проектировал. Ядром даты берутся ровно первые четырнадцать символов, поэтому метка с пятнадцатой цифрой, 202607191500000, спокойно разбирает свои первые четырнадцать и отдаёт лишний 0 считывателю суффикса, а тот видит символ, который знаком не является, и идёт по ветке без смещения. В парсере это значение возвращает 15:00 UTC, а лишняя цифра выбрасывается без единого слова. Для завершающего Z исход безобидный: тот же провал вниз обрабатывает и его, а он и так означает UTC. Для всего остального это молчаливое усечение. И безобиден случай с Z только потому, что предполагаемый часовой пояс в выпущенном приложении не выставляет ничто, но об этом отдельный раздел ниже.

Эти семь зафиксированы тестами как ошибки, и каждая называет реальный класс повреждения:

  • +2, час одной цифрой, слишком коротко, чтобы дойти до switch
  • +02:0, форма с двоеточием, в которой не хватает цифры
  • +0X30, буква внутри цепочки цифр
  • +2400, двадцать четыре часа
  • +99:00, девяносто девять часов
  • -0270, семьдесят минут
  • +, знак и больше ничего

Большую часть этих отказов дают две проверки. Минуты больше 59 отклоняются сразу, а суммарная величина ограничена 14 часами, потому что дальше не пускает сам тип даты и потому что, как сказано в комментарии к коду, «a guide claiming one is corrupt rather than exotic», то есть телегид, заявляющий такое, скорее битый, чем экзотический. Граница включающая, поэтому +1400 разбирается в час ночи по UTC, а +1401 не проходит. Проверка цифр к тому же требует именно ASCII-цифры и разбирает их без единого включённого числового стиля, так что ни ведущий знак, ни разделитель разрядов, ни полноширинная цифра не просочатся через снисходительный разбор с учётом культуры.

Две из трёх меток времени в примерах DTD наш считыватель отвергает

DTD разрешает не только четырнадцатизначную форму. Там сказано, что дата может быть YYYYMMDDhhmmss или её начальной подстрокой, и приведены три примера: 200007281733 BST, 200209 и 19880523083000 +0300. Наш парсер переживает только третий, разбирая его в 05:30 UTC. Второй не проходит по нижней границе длины, которая отсекает всё короче четырнадцати символов ещё до попытки разбора. Первый эту границу берёт и всё равно падает, потому что считыватель отрезает под ядро даты первые четырнадцать символов, а 200007281733 B датой не является.

Это сужение мы выбрали сами, и когда оно срабатывает, файл здесь ни при чём. У метки с точностью до минуты нечего отрезать под секунды, метка из года и месяца ни в каком полезном смысле не является началом программы, а считыватель, принявший любую из них, попросту выдумывал бы недостающие поля. Отказывать им можно и нужно. А вот выдавать их за повреждение нельзя, и последняя строка разбора ниже это соответствующий стандарту XMLTV, который мы выбрасываем.

Первый пример из DTD в одной скобке несёт вторую половину той асимметрии, о которой эта статья. 200007281733 BST снабжён там пометкой (BST == +0100.), то есть документ, определяющий формат, разрешает ровно то сокращение, разрешать которое наш считыватель отказывается.

Почему +0100 BST мы принимаем, а голому BST не верим

Здесь есть асимметрия, которая выглядит недосмотром, а на деле является решением. Суффикс +0100 BST разбирается в плюс один час, потому что число на месте, а метка выбрасывается. Суффикс из одного только BST разбирается в UTC, и это на час мимо для каждой британской летней даты, где он встретится. И то и другое зафиксировано тестами, то есть и то и другое сделано намеренно.

Именованные пояса не разрешаются никогда, и причина есть в комментарии: «the abbreviations are ambiguous across regions», сокращения неоднозначны от региона к региону. BST это British Summer Time, но так же записывали и Bangladesh Standard Time, а между ними пять часов. CST это столкновение, которое живёт в самой базе tz: его носят сразу Чикаго, Гавана и Шанхай. Разрешить имя значит выбрать одно из них, то есть угадать, а догадка, верная на одном рынке и неверная на другом, хуже правила, которое ошибается везде одинаково.

Мы выбрали ответ, заведомо неверный по фиксированному правилу, а не догадку, и стоит это часа на летних датах в любом фиде, который метит своё время буквами вместо цифр. Пометка, которую DTD поставил к собственному примеру, защищать эту позицию мешает сильнее, чем казалось до того, как статья была написана: документация самого формата одно сокращение всё-таки разрешила, а весь наш аргумент, почему мы этого до сих пор не делаем, это абзац выше.

Это настоящее ограничение, и насколько часто такая форма встречается, мы не знаем: голое имя пояса это ровно тот вход, который не даёт ни ошибки, ни счётчика, ни жалобы, пока кто-нибудь не сверит телегид с часами.

Строка диагностики считает отброшенные, но причину не называет

Метка времени, которая не разобралась, исключения не бросает. Атрибут просто не присваивается, start остаётся нулём, строка отклоняется на сборке, а счётчик растёт на единицу. Этот счётчик виден в диалоге настроек телегида у источника, под заголовком «Последняя загрузка гида», в таком виде:

{0} channels · {1} programmes · {2} dropped · {3} ms

Это единственная сводка, которую вы получаете, и она честна насчёт итога и молчалива насчёт причины. Одно и то же число увеличивают три никак не связанных условия: программа без атрибута channel, программа, у которой не разобралась метка start, и программа, выпавшая из окна хранения.

Большое число обычно приходит именно из окна, и окно тут не одно. Начинается оно всегда за 24 часа до момента разбора. Заканчивается через восемь дней для телегида отдельного источника, через четыре дня, если распакованная загрузка больше 200 МБ, и через два дня для семейного хранилища, которое держит меньше, потому что держит сразу все источники. Поэтому телегид, публикующий расписание на три недели, покажет очень большое число отброшенных при совершенно здоровом импорте, а тот же самый файл покажет ещё большее, стоит включить семейное хранилище. Большое число это повод посмотреть, а не доказательство проблемы со временем.

Неприятно здесь то, что самый громкий наш способ сообщить о сбое это одно целое число в диалоге, который никто не открывает, пока что-нибудь уже не выглядит сломанным.

Плохой stop стоит вам трёх часов, а не одной программы

Нечитаемое смещение в метке start отбрасывает строку. Нечитаемое смещение в метке stop не отбрасывает.

Когда start разобрался, а stop нет, stop остаётся нулём, не проходит проверку «stop позже, чем start», и строке выдаётся длительность по умолчанию, три часа. Отдельное ограничение режет любую программу до шести часов; здесь оно бездействует, потому что три меньше шести, и срабатывает только на строках, у которых stop таки разобрался и улетел далеко. Ни значение по умолчанию, ни ограничение счётчик отброшенных не трогают. То есть файл, у которого атрибуты stop испорчены так, как не испорчены атрибуты start, даёт телегид с правильным временем начала, трёхчасовыми блоками повсюду, наезжающими друг на друга строками и чистой сводкой импорта. Это частая картина за жалобами на телегид, где время вроде правильное, но ничего не сходится.

Шесть программ на входе, четыре на выходе

Вот фрагмент, собранный так, чтобы задеть сразу все ветки. Его прогнали через выпущенный парсер, а не разобрали на бумаге. Каждый идентификатор канала использует зарезервированный домен для примеров, все названия выдуманы, и считайте, что окно хранения эти даты покрывает, иначе хранение снимет весь разговор ещё до того, как дойдёт очередь до смещений.

<?xml version="1.0" encoding="UTF-8"?>
<tv>
  <channel id="one.example.com"><display-name>Example One</display-name></channel>
  <programme start="20260719150000 +0200" stop="20260719160000 +0200" channel="one.example.com">
    <title>Clean offset</title>
  </programme>
  <programme start="20260719160000 +02:0" stop="20260719170000 +02:0" channel="one.example.com">
    <title>Broken on both ends</title>
  </programme>
  <programme start="20260719170000 +02:00" stop="20260719180000 +02:0" channel="one.example.com">
    <title>Broken on the stop only</title>
  </programme>
  <programme start="20260719180000 BST" stop="20260719190000 BST" channel="one.example.com">
    <title>Zone name, no number</title>
  </programme>
  <programme start="20260719190000 +0100 BST" stop="20260719200000 +0100 BST" channel="one.example.com">
    <title>Zone name after a number</title>
  </programme>
  <programme start="20260719" stop="20260720" channel="one.example.com">
    <title>Date only</title>
  </programme>
</tv>

Строка один это счастливый путь: 15:00 при плюс двух часах это 13:00 UTC, и идёт она до 14:00 UTC.

У строки два +02:0 в обоих атрибутах. Суффикс начинается со знака, значит обязан прочитаться как смещение; цепочка длиной четыре символа, а последние два не обе цифры, поэтому разбор падает. start не присваивается, строка отклоняется, и счётчик отброшенных доходит до единицы.

У строки три чистый start и тот же сломанный stop. start разбирается в 15:00 UTC. stop не разбирается, поэтому применяется трёхчасовое значение по умолчанию, и строка занимает с 15:00 до 18:00 UTC вместо одного часа. Не засчитывается ничего. Именно из-за таких строк счётчик отброшенных не является метрикой здоровья.

Строка четыре это тот самый часовой случай в чистом виде. Суффикс BST со знака не начинается, поэтому считыватель идёт по ветке без смещения и применяет предполагаемый часовой пояс, а это UTC. Граббер имел в виду 17:00 UTC. Телегид показывает 18:00 UTC. Ни один счётчик не двинулся.

Строка пять несёт ту же метку, но после настоящего числа, и разбирается правильно, в 18:00 UTC. Выходит, строки четыре и пять начинаются в 18:00 UTC на одном канале, а намеревалась так только одна из них: именно так изнутри выглядит файл, за которым стоят два разных граббера.

В строке шесть восемь символов, ниже границы в четырнадцать, поэтому падают оба атрибута и счётчик отброшенных доходит до двух. И это же та строка, которую DTD назвал бы законной, начальная подстрока полной метки времени, так что половина посчитанного в этом фрагменте повреждения это наше сужение, а не файл.

До хранилища дошли четыре строки, и две из четырёх неправильны так, что об этом никто не сообщает: одна начинается на час позже, другая идёт втрое дольше настоящего. О двух оставшихся счётчик сообщает, но причину не называет ни для одной.

Смещение вычисляется для каждого момента, а не один раз на файл

Ядро даты разбирается вообще без флагов-предположений, и возвращается значение с неопределённым kind, и это сделано намеренно. Дальше считыватель явным образом связывает его со смещением. Альтернатива, отдать неопределённое значение конструктору, который сам заполнит пробел, заставляет .NET подставить собственное смещение машины, и тогда один и тот же файл дал бы разное расписание на устройстве в Париже и на устройстве в Тунисе, а хранилище перестало бы быть переносимым между двумя устройствами одной семьи.

На ветке без смещения смещение предполагаемого пояса ищется для конкретного момента, а не один раз на весь файл. При весеннем переводе часов в западноевропейском поясе две метки времени, между которыми по местным часам два с половиной часа, в UTC расходятся на девяносто минут, и это правильно, а единое смещение на весь файл ошибалось бы на каждой строке после перевода. Два местных времени, которые перевод делает непригодными, час, который случается дважды, и час, которого не бывает вовсе, разбираются со стандартным смещением оба, а повторный разбор возвращает то же самое значение, так что один файл не может превратиться в два разных телегида за два подряд идущих импорта.

В этом механизме есть честный пробел. Параметр предполагаемого часового пояса существует, оба пути загрузки телегида на устройстве его передают, и оба передают null. В выпущенном приложении его не выставляет ничто, и это зафиксировано тестом. Телегид, который пишет голое местное время по настенным часам без суффикса, читается как UTC, то есть по задокументированному умолчанию DTD, и никакой настройки на уровне источника, которая изменила бы разбор, нет. Всё, что описано в абзаце про переводы часов выше, это механизм, до которого сейчас не доходит ни один выпущенный путь кода.

Часовая поправка работает целыми часами, и в двух местах она отключена

Поправка есть, и работает она на другом уровне. У каждого источника есть сдвиг времени в целых часах, от минус 48 до плюс 48, и правится он в том же диалоге настроек телегида, в разделе «Сдвиг времени (целые часы)», в поле «Часов добавить». Применяется он при чтении расписания из хранилища, а не при записи в него, поэтому его изменение ничего не требует скачивать заново и не переписывает уже сохранённое.

Двух вещей он намеренно не делает. Он работает только целыми часами, поэтому получасовой пояс им не исправить: источник из региона +05:30, прочитанный как UTC, обратно им не вернуть. И он принудительно обнуляется в двух случаях. Первый это семейное хранилище телегида, которое при загрузке нормализуется в UTC, так что сдвигу там исправлять уже нечего. Второй это любой канал объединённого источника, за которым стоят сразу несколько участников, и правильное смещение зависит от того, кто из них сейчас отдаёт поток. Одно число оказалось бы неверным для всех, кроме одного, поэтому регулятор не показывают вовсе, вместо того чтобы показывать и врать.

Именно это последнее решение я пересмотрел бы первым, если бы появились новые данные. Оно защитимо, и оно же означает, что запасного выхода нет ровно там, где у семьи больше всего источников и больше всего шансов, что один из них врёт.

Наши приложения для Windows и Xbox делают весь этот разбор локально, по тому адресу телегида, который у вас уже есть. Строка диагностики и регулятор сдвига живут в одном диалоге не случайно: первое число говорит, читается ли файл вообще, а за вторым вы тянетесь, когда уже знаете, что читается.

Если вы пишете собственный считыватель, спасёт вас то правило, которое далось нам дольше всего. Суффикс, который начинается со знака, уже сказал вам, что он число. Прочитайте его или отклоните строку, но не проваливайтесь в значение по умолчанию: умолчание применяется одинаково и уверенно ко всем меткам времени в файле, а одинаковая уверенная неправота это единственный сбой, о котором никто не сообщает.

Что измеряется в этой статье28 заявлений, каждое из которых имеет под собой доказательства
ПретензияДоказательстваПодсчитано
Определение типа документа XMLTV говорит, что метку времени без явно указанного часового пояса следует читать как UTC.xmltv.dtd, date field documentation: you can also append a timezone to the end; if no explicit timezone is given, UTC is assumed.СпецификацияНе применимо
Собственный пример смещения в XMLTV DTD записан в базовой четырёхзначной форме, как +0300.xmltv.dtd, date field documentation, example 19880523083000 +0300СпецификацияНе применимо
XMLTV DTD разрешает записать дату как YYYYMMDDhhmmss или как любую её начальную подстроку и приводит три примера: 200007281733 BST, 200209 и 19880523083000 +0300.xmltv.dtd, date field documentation, including the annotation (BST == +0100.) on the first exampleСпецификацияНе применимо
ISO 8601 записывает смещение от UTC в трёх формах: базовой (+0200), расширенной (+02:00) и только в часах (+02).ISO 8601-1, time offset from UTCСпецификацияНе применимо
.NET DateTimeOffset отклоняет смещение, которое по модулю превышает 14 часов, и принимает ровно 14 часов..NET DateTimeOffset constructor, offset argument range -14:00 to +14:00 inclusiveСпецификацияНе применимо
Если построить DateTimeOffset из DateTime, у которого Kind равен Unspecified, применяется локальное смещение самой машины..NET DateTimeOffset(DateTime) constructor remarks, which treat an Unspecified Kind as local timeСпецификацияНе применимо
Сокращения часовых поясов не уникальны: CST носят сразу America/Chicago, America/Havana и Asia/Shanghai, а BST это British Summer Time, но так же записывали и Bangladesh Standard Time, а между ними пять часов.IANA time zone database, theory.html on the ambiguity of zone abbreviations, plus the CST abbreviation on America/Chicago, America/Havana and Asia/ShanghaiСпецификацияНе применимо
Суффикс метки времени, начинающийся с + или -, обязан разобраться как числовое смещение, иначе не разбирается вся метка целиком. Провал в предполагаемый часовой пояс это тот самый выпущенный в релиз дефект, на смену которому пришло это правило.n = 222 авг. 2026 г.
Принимаются только цепочки цифр длиной 2, 4 или 5 с двоеточием на позиции 2, поэтому +02, +0200 и +02:00 разбираются все; любая другая длина вываливается в ветку default.n = 322 авг. 2026 г.
Семь форм смещения зафиксированы тестами как ошибки разбора: +2, +02:0, +0X30, +2400, +99:00, -0270 и одинокий +.n = 722 авг. 2026 г.
Минуты больше 59 отклоняются, а суммарная величина ограничена 14 часами включительно, поэтому +1400 разбирается, а +1401 нет; двухзначный считыватель требует именно ASCII-цифры и разбирает их с NumberStyles.None.n = 222 авг. 2026 г.
Метка пояса после настоящего смещения отбрасывается по первому пробелу, поэтому +0100 BST разбирается в +01:00, а местное время 15:00 даёт 14:00 UTC.n = 122 авг. 2026 г.
Голое буквенное имя пояса до считывателя смещения не доходит и идёт по ветке предполагаемого часового пояса, поэтому и 20260719150000 BST, и 20260719150000 UTC дают 15:00 UTC.n = 222 авг. 2026 г.
Две из трёх меток времени, приведённых в XMLTV DTD как примеры, наш считыватель отвергает: 200007281733 BST и 200209 не проходят, а разбирается только 19880523083000 +0300, в 05:30 UTC.n = 322 авг. 2026 г.
Метка времени короче 14 символов отклоняется ещё до любой попытки разбора, поэтому значение из одной только даты не проходит.n = 122 авг. 2026 г.
Ядро даты это ровно первые 14 символов, поэтому лишняя 15-я цифра попадает в суффикс, не проходит проверку на знак и молча отбрасывается.n = 122 авг. 2026 г.
Правило жёсткой ошибки смотрит на первый символ суффикса, поэтому форма с меткой впереди, вроде GMT+0200, идёт по ветке предполагаемого часового пояса и читается как UTC, а смещение игнорируется.n = 122 авг. 2026 г.
Один и тот же счётчик отброшенных растёт и при отсутствующем атрибуте channel, и при неразбираемом start, и при программе за пределами окна хранения.n = 322 авг. 2026 г.
Окно хранения всегда начинается за 24 часа до момента разбора и заканчивается через восемь дней на пути отдельного источника, через четыре дня, если распакованная загрузка превышает 200 МБ, и через два дня для семейного хранилища.n = 322 авг. 2026 г.
Программа с корректным start и неразбираемым stop сохраняется с длительностью по умолчанию в три часа и в отброшенные не попадает; отдельный шестичасовой максимум по ней не срабатывает.n = 122 авг. 2026 г.
Сводка последней загрузки выводится как каналы, программы, отброшено и миллисекунды, под заголовком «Последняя загрузка гида».n = 122 авг. 2026 г.
Поправка на уровне источника задаётся целыми часами, от -48 до +48, подписана как «Сдвиг времени (целые часы)» с полем «Часов добавить» и применяется при чтении, а не при загрузке.n = 122 авг. 2026 г.
Поправка принудительно обнуляется, когда активно семейное хранилище телегида и когда канал принадлежит объединённому источнику.n = 222 авг. 2026 г.
Оба пути загрузки на устройстве создают конвейер разбора с assumeTimezone равным null, поэтому голая метка времени читается как UTC, а часовой пояс машины в дело не вступает.n = 222 авг. 2026 г.
На ветке предполагаемого часового пояса смещение вычисляется для каждого момента отдельно, поэтому два местных времени, между которыми два с половиной часа через весенний перевод, в UTC расходятся на девяносто минут.n = 222 авг. 2026 г.
И повторяющийся местный час, и несуществующий местный час разбираются со стандартным смещением, а повторный разбор возвращает то же значение.n = 322 авг. 2026 г.
Собранный вручную фрагмент из этой статьи даёт 1 канал, 4 сохранённые строки и 2 отброшенные; одна сохранённая строка начинается на час позже, а другая идёт три часа вместо одного.n = 622 авг. 2026 г.
В большем из двух плейлистов нашего корпуса M3U 527 строк из 1895 в итоге остаются без пригодного идентификатора канала, так что строке телегида не к чему надёжно привязаться.n = 189522 авг. 2026 г.