Расписание Турниского политехнического университета в Ташкенте
Веб-приложение (PWA) и Telegram-бот в одном процессе.
Открывается мгновенно, живёт без интернета, говорит на двух языках.
✨ Возможности · 📸 Скриншоты · 🚀 Быстрый старт · ⚙️ Настройки · 🔌 API · 📴 Офлайн · 🔬 Данные · 🐳 Docker · 💬 Контакты
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
| Главная | Неделя | День | Группы |
![]() |
![]() |
![]() |
![]() |
| Меню | Тёмная тема | Неделя (тёмная) | Без интернета |
Последний кадр снят в режиме полёта: расписание отрисовано из кэша на устройстве.
| 🗂 Три среза | Расписание по группам, преподавателям и аудиториям — на неделю или на день |
| ⏱ «Сейчас идёт / Далее» | Подсветка текущей и следующей пары, полоса прогресса, обратный отсчёт. Считается на клиенте — не требует перерисовки страницы |
| 📴 Офлайн-режим | Расписание скачивается на устройство и открывается без сети. Как это работает |
| 🌍 Русский и английский | Переключение мгновенное, без перезагрузки |
| 🌗 Светлая и тёмная темы | Плюс режим «Авто» по системной настройке |
| 🔍 Поиск | По группам, преподавателям и аудиториям; локально и с сервера |
| 📲 Установка на телефон | PWA: «Добавить на главный экран» — иконка, свой заголовок, полноэкранный режим |
| 🎨 Дизайн без шаблонов | Редакционная типографика, самохостные шрифты, никаких UI-библиотек |
| 👥 Выбор группы | С поиском и постраничным списком |
| 📅 Быстрые срезы | Сегодня, Завтра, Вся неделя, Ближайшая пара |
| 👤 Преподаватели и аудитории | Расписание любого преподавателя или аудитории |
| 🌅 Утренний дайджест | Пары на день в заданное время |
| ⏰ Напоминание перед парой | За N минут до начала |
| ⚙️ Настройки в чате | Уведомления включаются инлайн-кнопками, без веб-интерфейса |
flowchart LR
A["🌐 EduPage<br/>ttpu.edupage.org"] -->|"JSON API<br/>POST /timetable/server"| B["⚙️ turin.sync<br/>парсер + планировщик"]
B -->|"денормализованный<br/>SQLite"| C[("🗄️ data/timetable.db")]
C --> D["🚀 FastAPI<br/>/api/*"]
D --> E["📱 PWA<br/>offline-first · RU/EN"]
D --> F["🤖 Telegram-бот<br/>дайджест · напоминания"]
style A fill:#f0e6d2,stroke:#b4552b,color:#3a3226
style C fill:#2f2a24,stroke:#b4552b,color:#f4f1ea
style E fill:#e8efe6,stroke:#3f6b3f,color:#3a3226
style F fill:#e3eef5,stroke:#2ca5e0,color:#3a3226
Один процесс делает всё: тянет расписание с EduPage, складывает в SQLite, отдаёт HTTP-API с веб-приложением и параллельно крутит Telegram-бота.
Ключевое решение — денормализованное хранилище: одна строка на
(неделя, вид владельца ∈ class | teacher | room, владелец, день, пара).
Благодаря этому один и тот же урок доступен и группе, и преподавателю, и
аудитории — без JOIN-ов на чтении и без риска рассинхронизации.
Нужен Python 3.11+ (используется zoneinfo).
git clone https://github.com/u2kdev/turintime.git
cd turintime
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Linux / macOS
pip install -r requirements.txt
copy .env.example .env # Windows (cp .env.example .env на Linux/macOS)Откройте .env и впишите токен бота — необязательно, без него веб-приложение
работает полностью:
BOT_TOKEN=123456789:AA... # токен от @BotFather
BOT_USERNAME=your_bot # без @, нужен только для ссылок в интерфейсеЗапуск:
python main.py # веб-сервер + бот
python main.py update # только скачать расписание и выйти| 🌐 Приложение | localhost:8000 |
| 📖 Документация API | localhost:8000/api/docs |
| 💚 Проверка живости | localhost:8000/api/health |
Первый запуск скачивает расписание с EduPage (несколько секунд) и складывает его в
data/timetable.db. Дальше обновление идёт фоном каждыеREFRESH_HOURSчасов.
- Напишите @BotFather →
/newbot→ задайте имя и username. - Скопируйте токен вида
123456789:AA...вBOT_TOKEN. - Впишите username без
@вBOT_USERNAME— тогда в интерфейсе появится кнопка «Открыть бота». - Перезапустите
python main.py. В логе появитсяTelegram-бот → включён.
💡 Бот работает через long polling — домен, HTTPS и вебхук не нужны.
⚠️ Если запущено несколько экземпляров приложения, бот должен быть включён только на одном (RUN_BOT=falseна остальных), иначе Telegram вернёт конфликт polling.
🛠 Вспомогательные скрипты (нужны только при работе над дизайном)
python scripts/fetch_fonts.py # скачивает woff2 в web/assets/fonts
python scripts/make_icons.py # рисует иконки PWA в web/icons
python tests/shoot_web.py # снимает скриншоты экранов
python scripts/export_screenshots.py # собирает галерею для этого READMEДля скриншотов нужен Playwright:
pip install playwright
playwright install chromiumВсе параметры читаются из .env. Полный список с комментариями — в
.env.example.
| Переменная | По умолчанию | Назначение |
|---|---|---|
BOT_TOKEN |
— | Токен бота от @BotFather. Пусто → бот выключен |
BOT_USERNAME |
— | Имя бота без @; из него собирается ссылка https://t.me/<username> |
BASE_URL |
http://localhost:8000 |
Внешний адрес приложения (ссылки, PWA) |
HOST / PORT |
0.0.0.0 / 8000 |
Где слушает сервер |
ADMIN_TOKEN |
— | Токен для POST /api/refresh. Пусто → доступ только с localhost |
EDUPAGE_ORIGIN |
https://ttpu.edupage.org |
Источник расписания |
SCHOOL_NAME / SCHOOL_SHORT |
TTPU | Название в интерфейсе |
TIMEZONE |
Asia/Tashkent |
Часовой пояс для «сейчас» и рассылок |
REFRESH_HOURS |
6 |
Как часто перекачивать расписание |
REFRESH_ON_START |
true |
Обновлять при старте сервера |
RUN_BOT |
true |
Выключить бота, оставив веб-приложение |
DAILY_DEFAULT_TIME |
07:30 |
Время утреннего дайджеста по умолчанию |
REMINDER_MINUTES |
15 |
За сколько минут предупреждать о паре |
Базовый префикс — /api, формат — JSON. {kind} — одно из class, teacher,
room. {id} — идентификатор EduPage, например *98 (IT1-26); URL-кодируется.
| Метод | Путь | Описание |
|---|---|---|
GET |
/api/health |
Жив ли сервис и готова ли база |
GET |
/api/meta |
Сводка: школа, время сервера, статистика, ссылка на бота |
GET |
/api/weeks |
Список учебных недель |
GET |
/api/directory |
Группы, преподаватели, аудитории, предметы, периоды, «популярные» |
GET |
/api/search?q= |
Поиск по справочнику |
GET |
/api/schedule/{kind}/{id} |
Расписание на неделю. ?week=<номер> или ?day=ГГГГ-ММ-ДД |
GET |
/api/now/{kind}/{id} |
Текущая и ближайшая пара |
GET |
/api/offline/{kind}/{id} |
Компактный пакет всех недель для офлайн-кэша |
POST |
/api/refresh |
Ручное обновление (нужен заголовок X-Admin-Token) |
GET |
/api/refresh/status |
Состояние последнего обновления |
Пример ответа /api/now/class/*98
{
"now": "2026-10-19T12:26:43+05:00",
"today": { "idx": 0, "date": "2026-10-19", "ru": "Понедельник", "count": 1 },
"current": {
"key": "*557",
"period": 3,
"start": "12:00",
"end": "13:20",
"subject": "English for Academic Purposes",
"short": "EAP",
"color": "#99FF33",
"teachers": [{ "name": "FAYZIYEVA GULASAL", "short": "G.FAYZIYEVA" }],
"rooms": [{ "name": "C405", "short": "C405" }]
},
"next": { "short": "DELS (lec)", "start": "12:00", "end": "13:20" },
"current_left_minutes": 53,
"progress": 0.334
}Задача: открыв приложение один раз с интернетом, дальше видеть расписание даже без сети.
| Слой | Что кэшируется | Где лежит |
|---|---|---|
| 1️⃣ Оболочка | HTML, CSS, JS, шрифты, иконки | Cache Storage (service worker) |
| 2️⃣ Справочник | Группы, преподаватели, аудитории, метаданные | localStorage |
| 3️⃣ Расписание | Все недели выбранной сущности | localStorage — ключ ttpu.bundle.v1.class.<id> |
| 4️⃣ Страховка | Ответы /api/offline/* |
Cache Storage, стратегия network-first |
- Оболочка. Service worker (
web/sw.js) кладёт статику вCache Storage— повторный запуск отдаётся из кэша мгновенно. - Данные. Справочник и метаданные сохраняются в
localStorage. Расписание группы — целиком, во все доступные недели — кладётся туда после первого открытия или по кнопке «Сохранить офлайн». - Без сети. Интерфейс читает
localStorage: рисует сохранённое расписание и показывает плашку «Офлайн · данные от …». Блоки «Сейчас идёт» и «Далее» считаются локально, поэтому тоже работают. - Страховка. Запросы
/api/offline/*идут по стратегии network-first с откатом вCache Storage— на случай, еслиlocalStorageочистили, а сеть пропала.
Проверка вручную: откройте приложение, выберите группу, включите режим полёта и перезагрузите страницу — расписание останется на месте.
Парсер работает напрямую с публичным JSON-API EduPage — без Playwright и браузера, в отличие от классических скрейперов.
Все запросы — POST на {EDUPAGE_ORIGIN}/timetable/server/<endpoint> с заголовком
Content-Type: application/json; charset=UTF-8 и телом:
{ "__args": [null, 2026], "__gsh": "00000000" }| Endpoint | __args |
Что возвращает |
|---|---|---|
ttviewer.js?__func=getTTViewerData |
[null, <год начала учебного года>] |
Список учебных недель (tt_num, даты, default_num) |
regulartt.js?__func=regularttGetData |
[null, "<tt_num>"] |
Таблицы одной недели: уроки, группы, преподаватели, аудитории, предметы, периоды |
⚠️ Подводные камни, на которые легко наступить
- Форма
application/x-www-form-urlencodedвозвращает{"reload": true}— нужен именно JSON. datefromучебной недели — воскресенье;dateto = datefrom + 6. Учебные дни — понедельник…суббота, поэтому «понедельник» недели =datefrom + 1 день.- Дни кодируются битовой маской; в одной ячейке может быть несколько
подгрупп — например, суббота разбита на
Group 1/Group 2с разными аудиториями. - Список недель меняется: прошедшие недели EduPage убирает. Приложение всегда показывает то, что отдаёт источник.
docker compose up --build -dПриложение — http://localhost:8000, данные (SQLite) — в томе ttpu-data.
Через docker run
docker build -t ttpu-timetable .
docker run -d --name ttpu -p 8000:8000 \
-e BOT_TOKEN=123456789:AA... \
-e BOT_USERNAME=your_bot \
-v ttpu-data:/app/data \
ttpu-timetableСкрипты в tests/ не требуют pytest и запускаются как есть.
python tests/check_parser.py # разбор одной недели напрямую из EduPage
python tests/test_api.py # все /api/* через TestClient
python tests/test_bot.py # тексты, клавиатуры, «ближайшая пара», хранилище
python tests/shoot_web.py # скриншоты экранов (нужен playwright)| Слой | Выбор | Почему |
|---|---|---|
| HTTP | FastAPI + uvicorn | Асинхронность, автодокументация из коробки |
| Источник | httpx | Тот же async-стек, без блокирующих вызовов |
| Хранилище | SQLite (stdlib) | Ноль зависимостей и настройки, файл рядом с приложением |
| Бот | aiogram 3 | Long polling, инлайн-клавиатуры, FSM |
| Фронтенд | Vanilla JS, без сборки | Открывается мгновенно, нечего собирать и обновлять |
| Стили | CSS custom properties | Токены дизайн-системы, темы одним переключателем |
| Шрифты | Literata, Manrope, JetBrains Mono | Самохостные woff2, 194 КБ на девять файлов |
| Офлайн | Service Worker + Cache Storage + localStorage |
Нативный механизм браузера, без библиотек |
📁 Структура проекта
main.py точка входа (uvicorn + бот)
turin/
config.py настройки из .env
edupage.py клиент и парсер EduPage
db.py схема SQLite, запросы, справочник
sync.py скачивание, запись в базу, планировщик
schedule.py «сейчас / далее», сетка дня, офлайн-пакет
api.py FastAPI: /api/*, отдача SPA
bot/
runner.py запуск polling, регистрация команд
handlers.py команды и callback-и
keyboards.py инлайн-клавиатуры
texts.py шаблоны сообщений
notifier.py утренний дайджест и напоминания
storage.py пользователи и подписки (SQLite)
web/
index.html разметка всех экранов
assets/app.css дизайн-система
assets/app.js маршрутизация, офлайн-кэш, i18n, «сейчас/далее»
assets/fonts/ самохостные woff2
sw.js service worker
manifest.webmanifest PWA-манифест
scripts/ генераторы шрифтов, иконок и галереи
tests/ проверки и скриншоты
docs/screenshots/ галерея для этого README
data/ база расписания и пользователей бота (git-ignored)
Вопросы, идеи и баг-репорты — в Telegram:
Расписание принадлежит Турнискому политехническому университету в Ташкенте. Приложение только отображает открытые данные EduPage.
Сделано с ♥ для студентов TTPU














