Перейти к основному содержанию

freshgo: порт FreshRSS на Go, который перестал быть портом

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

Читалки при этом я менял. Долгое время пользовался miniflux и даже переносил его на отдельный хостинг1. Потом перешёл на FreshRSS и поставил его на свой сервер2. Он меня в целом устраивал: ленты обновлялись, клиенты на телефоне работали. Но постоянно что-то не нравилось, по мелочи и в разных местах, и ни одна из этих мелочей не тянула на причину переезжать.

В итоге я сделал свою читалку: с помощью Claude написал freshgo .

Экран чтения freshgo в оформлении Reader classic

Сначала порт

Задумывался freshgo как полный порт FreshRSS на Go: один исполняемый файл, который забирает данные работающей установки и встаёт на её место. Первый коммит сделан утром 6 октября. К вечеру того же дня в репозитории были хранение в SQLite и PostgreSQL, импорт из FreshRSS, разбор лент всех видов, которые умеет оригинал, его язык поиска и фильтры, Google Reader API и каркас веб-интерфейса3.

Образцом служил запущенный FreshRSS 1.30.1. В репозитории лежит то, что он выдал на фиксированном наборе лент и что ответил на список запросов API, и тесты сверяют поведение freshgo с этими ответами4.

Переезд занимает одну команду: freshgo import читает каталог данных FreshRSS и переносит пользователей с паролями, ленты с настройками, статьи с отметками о прочтении, метки и значки. Мои 84 ленты и шесть тысяч статей импорт перенёс за 1,1 секунды. Обратной дороги нет, базу freshgo в FreshRSS не вернуть, поэтому каталог оригинала лучше сохранить, пока не убедитесь, что всё на месте.

Клиентам о переезде знать не нужно. FreshRSS отдаёт Google Reader API по адресу /api/greader.php, и freshgo отвечает по тому же пути, с прежними паролями API, уже выданными токенами и идентификаторами статей. Тот же API доступен и по обычным путям Google Reader, /accounts/ и /reader/, так что клиенту можно дать просто адрес сервера. Fever API не реализован.

Потом своё

Чистым портом freshgo пробыл недолго. Версия 0.1.0 вышла 8 октября, и уже в ней было то, чего в оригинале нет; остальное добавилось за следующие два дня.

Интерфейс Google Reader. Веб-интерфейс у freshgo свой, и собран он по образцу Google Reader: слева дерево категорий и лент, справа статьи по одной строке, статья раскрывается на месте. Оформлений два, «Reader classic» и «Reader 2011», по двум версиям оригинала. У каждого есть светлые и тёмные цвета; тёмные пришлось придумывать, у Google Reader их не было. Страницы собирает сервер, любое действие остаётся ссылкой или формой и работает без JavaScript, а скрипт добавляет клавиши и действия без перезагрузки страницы.

Оформление Reader 2011 с тёмными цветами

Раскрытая статья: действия стоят между заголовком и текстом

Телефон и PWA. Под узкий экран интерфейс доработан отдельно. Меню и управление списком убираются за кнопки. Касание строки раскрывает статью и закрывает предыдущую. Внизу закреплена панель: следующая и предыдущая статья, отметка о прочтении, звёздочка. freshgo можно установить как приложение, с телефона через «На экран Домой», с компьютера через «Установить». Для этого нужен HTTPS, и читать без сети нельзя: это тот же сайт в отдельном окне.

Картинки через сервер. Обычно картинки статьи браузер грузит с тех сайтов, где они лежат, и эти сайты видят адрес читателя и время, когда он открыл статью. freshgo может отдавать картинки со своего адреса. Схему я взял у miniflux5: адреса подменяются в момент показа, каждый подписан, и сервер загружает только то, что сам вставил в статью. Режимов три: все картинки, только те, что отдаются по http (их страница на https иначе не покажет вовсе), и никакие. Картинки нигде не сохраняются, сервер передаёт их насквозь. Работает это и в веб-интерфейсе, и для клиентов Google Reader API.

Полный текст. Многие ленты отдают только анонс. FreshRSS умеет забирать текст со страницы статьи по CSS-селектору, и селектор для каждой ленты надо написать самому. В freshgo к этому добавились три вещи:

  • статья находится на странице без селектора, так же, как её находит режим чтения в браузере;
  • текст можно запросить для одной статьи клавишей f, что бы ни было настроено у ленты;
  • страницы ленты может читать настоящий браузер.

Третий пункт появился из-за сайтов за проверкой Cloudflare. Лента у openai.com читается, а на запрос страницы статьи сервер получает 403 и «Just a moment…», напрямую и через прокси, с User-Agent и curl, и Firefox6. Теперь рядом с freshgo можно запустить браузер с протоколом DevTools и отметить ленты, страницы которых читает он. Из того, что я пробовал, самым лёгким оказался Lightpanda : около 30 МиБ в простое и несколько сотен, пока он читает страницу. Проверку, которая требует щёлкнуть или собрать картинку, так не пройти; статья в этом случае остаётся с текстом из ленты.

Перевод. У раскрытой статьи есть действие «Перевести», клавиша T. Переводит языковая модель: подходит любой сервис с API, совместимым с OpenAI, адрес, ключ и модель задаются тремя настройками сервера. Текст уходит в модель по абзацам и появляется на экране по мере готовности. Разметку модель не видит совсем: ссылки, картинки и код заменяются номерными метками и возвращаются на место без изменений. Готовый перевод хранится вместе со статьёй, второй раз за него платить не придётся. Помнить стоит одно: текст статьи при каждом запросе перевода уходит с вашего сервера в тот сервис, который вы назвали.

WebSub. Если лента объявляет хаб, freshgo подписывается на него, и новые статьи приходят в момент публикации; сама лента после этого опрашивается раз в сутки. В первой версии подписка зависела от опроса: пока документ ленты не менялся, сервер к хабу не обращался, и редко обновляемая лента могла неделями оставаться без подписки. Теперь адрес хаба хранится вместе с лентой, читается и из заголовков Link, а подписки продлеваются по сроку7.

Остальное перечислю коротко: вход через OpenID Connect, один прокси на все ленты с возможностью задать лентам свой, управление с клавиатуры с палитрой команд по Ctrl+K.

Что показали замеры

Писать на Go ради того, чтобы было на Go, смысла мало, поэтому я попросил сравнение. Оба сервера работали на одной машине с одними и теми же подписками: один пользователь, 84 ленты, около 6000 статей, база в SQLite8.

FreshRSS 1.30.1freshgo
Процессорное время за сутки109 с95 с
Память без дискового кэша, средняя / наибольшая116 / 178 МиБ42 / 64 МиБ
Образ контейнера384 МБ38,3 МБ
Данные на диске189 МиБ49 МиБ
Записано на диск за сутки1,2 ГиБ0,54 ГиБ

Процессора оба тратят одинаково мало: время обновления лент уходит на ожидание чужих сайтов, а не на вычисления. Памяти freshgo нужно втрое меньше. На диске разница вчетверо, и почти вся она в кэше: FreshRSS хранит копию каждого загруженного документа ленты, freshgo запоминает только ETag и Last-Modified. Сами статьи занимают в обеих базах одинаково.

Со скоростью ответов получилось интереснее. В первом замере freshgo версии 0.2.3 проиграл оригиналу именно там, где клиент проводит время при синхронизации:

ЗапросFreshRSSfreshgo 0.2.3freshgo 0.2.7
stream/contents, 50 статей4,6 мс7,3 мс3,3–3,7 мс
stream/contents, 200 статей13,8–18,4 мс25,7 мс11,3–11,7 мс
Список непрочитанных1,5 мс9,7 мс0,7 мс
Главная страница, 20 непрочитанных8,8 мс10,2 мс5,3–5,8 мс
stream/contents, 8 соединений694 запроса/с426 запросов/с1118–1125 запросов/с

Причины нашлись обычные. Список непрочитанных открывал каждую строку пользователя ради одного флага. Текст статьи чистился заново при каждом показе. Адреса картинок подменялись через полный разбор HTML. После индексов, чистки текста при записи и подмены адресов без разбора документа freshgo отвечает быстрее FreshRSS на девяти запросах из десяти и наравне на одном.

Для одного читателя всё это несколько миллисекунд в любую сторону. Разница станет заметной при многих пользователях или с клиентами, которые забирают статьи тысячами.

Чего ждать

  • Проекту меньше недели. За это время вышло двенадцать версий, считая три предварительные; последняя 0.2.7.
  • Проверен он одной установкой, моей: один пользователь, SQLite. PostgreSQL поддерживается и покрыт тестами, но в сравнении не участвовал.
  • Импорт читает FreshRSS на SQLite и PostgreSQL. MySQL и MariaDB он не читает.
  • PHP-расширения FreshRSS не работают. Точки расширения есть, но обработчики для них собираются вместе с сервером.
  • Часть поведения отличается от оригинала намеренно, список есть в README9.
  • Это независимый проект. Вопросы по нему авторам FreshRSS задавать не нужно.

Теперь на моём сервере работает freshgo, и я могу рекомендовать его вам. Код и инструкции лежат в репозитории juev/freshgo , образ опубликован как ghcr.io/juev/freshgo. Если что-то работает не так или чего-то не хватает, заведите issue.


  1. Miniflux on Heroku . ↩︎

  2. Настройка Docker для работы с локальным прокси-сервером : заметка времён переноса FreshRSS на свой сервер. ↩︎

  3. История коммитов juev/freshgo . ↩︎

  4. testdata/reference и спецификации в docs/specs . ↩︎

  5. Issue #4 : разбор того, как отдаёт картинки miniflux, и что из этого взято. ↩︎

  6. Issue #55 : что было испробовано против проверки Cloudflare и сколько памяти занимает каждый вариант. ↩︎

  7. Issue #18 . ↩︎

  8. freshgo and FreshRSS measured side by side — условия, полные таблицы и способ повторить. Процессор, память и диск измерены у версии 0.2.3, время ответов у версии 0.2.7. ↩︎

  9. Раздел What behaves differently . ↩︎