Skip to content

v1.0.4

Latest

Choose a tag to compare

@github-actions github-actions released this 23 May 10:07

Поверх v1.0.3 — две большие функциональные фичи (режим «лента тега» + умная разбивка папки на подпапки), один внутренний рефактор сетевого слоя (горячее переключение SOCKS5/onion без перезапуска приложения) и набор устойчивости к JR-CDN-WAF. Совместимость с v1.0.3 полная, существующие пресеты и манифесты переносить не нужно.

Главное в этом релизе — режим «лента тега» (скачивание из лент Бездна / Лучшее / Хорошее / Новое конкретного тега, а не только через Query.search), разбивка папки на part-001, part-002 с гарантией, что пост никогда не разорвётся между подпапками, и горячее применение сетевых настроек — теперь Tor можно включать/выключать прямо в работающем приложении, сессия логина не теряется.

Режим «лента тега» (Tag.postPager)

Селектор сортировки превратился из 2-кнопочного («рейтинг / дата») в 6-кнопочный:

[ рейтинг ] [ дата ] | [ бездна ] [ лучшее ] [ хорошее ] [ новое ]
   search                       Tag.postPager (PostLineType)

Первые две — старый режим Query.search с полным набором серверных фильтров (рейтинг, NSFW, автор, исключения тегов, сортировка).

Четыре новых — переключают пайплайн на Tag.postPager с соответствующим PostLineType:

  • безднаPostLineType.ALL (всё, что есть в теге).
  • лучшееPostLineType.BEST.
  • хорошееPostLineType.GOOD.
  • новоеPostLineType.NEW.

Это ровно те же ленты, что на сайте джоя при клике на тег. У Tag.postPager нет серверных фильтров, поэтому требования:

  • Требуется ровно один тег в поле тегов (это всё, что принимает API). UI выводит подсказку под селектором, если тегов не один.
  • Фильтры рейтинга и NSFW применяются клиент-сайд — мы тянем посты с сервера и сами отфильтровываем то, что не подходит. Это означает, что если в ленте мало постов с нужным рейтингом, скачивание просто отдаст меньше файлов, чем заявленный лимит.
  • Сортировка ленты задаётся сервером (фактический порядок постов в ленте джоя), локальная пересортировка отключена — иначе перетасует то, что JR уже выстроил.

Пресет тоже умеет запоминать режим: в JSON-пресете теперь есть поле feed (наряду со старым sort), которое выигрывает над sort, если стоит. Старые пресеты с sort: "rating"/"date" продолжают работать без миграции.

Разбивка папки на part-001, part-002… по постам / страницам / файлам

В ⚙ → Структура папки появилась пара полей:

| Разбивать на подпапки part-001, part-002… по N (0 — не разбивать)  |
| Считать в чём: [ постах ▼ ]                                         |

Когда в задаче набегают тысячи файлов, Проводник/Finder начинают тормозить на плоском списке. С разбивкой каждые N единиц открывается новая подпапка part-XXX. Манифест продолжает дедуплицировать как обычно.

Главное отличие от того, как обычно делают такие штуки — что такое «N единиц»:

  • постах (по умолчанию) — границы между подпапками падают между постами. Многокартиночный комикс / фотосет всегда уезжает в одну part-XXX целиком. Это то, чего просили в комментарии к v1.0.3.
  • страницах — одна part-XXX на N страниц GraphQL-ленты. Посты по-прежнему не рвутся (страница ≥ пост), плюс легко мапить «страница 31-60» на конкретную папку.
  • файлах — точный счётчик по картинкам. Самый предсказуемый по числу файлов в папке, но многокартиночный пост может разорваться между двумя part-XXX, если попадёт на границу.

Резюме при повторных запусках (если задача в той же папке уже частично отработала):

  • Режим постах / страницах: всегда открываем part-(max+1) — мы не можем по содержимому диска определить, сколько постов лежит в существующей part-XXX, и риск разорвать пост перевешивает «лишнюю почти пустую папку».
  • Режим файлах: продолжаем заполнять самую последнюю part-XXX (если в ней меньше N файлов), как было в первой реализации.

Технически это работает так: продюсер задачи резервирует имя подпапки до того, как воркеры подбирают конкретные Job'ы (новое поле Job.SubdirHint), и пишет его в каждую задачу одного поста. Параллельные воркеры не могут переставить картинки одного поста в разные подпапки, потому что подпапка уже зафиксирована в задаче.

⚠️ При включённой разбивке файл считается уже скачанным только если он есть в манифесте (per-folder os.Stat-fast-path работает только в режиме без разбивки — мы не знаем, в какой part-XXX искать). Если у тебя на диске лежат старые файлы без манифеста — сначала запусти «🔄 Полностью пересобрать манифест» из v1.0.3.

Горячее применение SOCKS5 / .onion / троттлинга — без перезапуска

В v1.0.3 любое изменение в секции ⚙ → Сеть (включить/выключить SOCKS5, поменять адрес прокси, выставить .onion-зеркало) требовало перезапуска приложения — мы делали *graphql.Client один раз на старте и больше не трогали. После перезапуска приходилось логиниться заново, если cookies протухли.

В v1.0.4 эти настройки применяются на лету:

  • graphql.Client.SetTransport(t) атомарно подменяет http.RoundTripper под sync.RWMutex. Cookie jar (где живёт сессия логина) — тот же самый объект, ничего не теряется.
  • На уровне GUI это вызывается из нового applyNetworkSettings(), который выбирается и при старте приложения, и при сохранении настроек в модалке. Никаких различий между «свежий запуск» и «настройка только что поменялась» — один и тот же кодовый путь.
  • Из подсказки в UI убрана прошлая фраза «после смены сетевых настроек — перезапусти приложение».

Это даёт удобный сценарий: можешь начать запускать задачу без Tor → увидеть, что JR-CDN начал выдавать 403 → включить SOCKS5 → задача продолжит работать через прокси без рестарта приложения. Запущенные задачи переключатся на новый транспорт через первый же *Client.Do после смены настроек.

Устойчивость к JR-CDN: backoff 403/429/5xx + конфигурируемая пауза между запросами

Реактор для CDN-WAF использует не «честный» HTTP 429, а тихие HTTP 403 без Retry-After (наблюдается опытным путём — никакого хедера, окно блока ~минуты). До v1.0.4 этот 403 пробрасывался наверх как «ошибка задачи», и пользователь сам видел кучу красных ✖ в очереди.

В v1.0.4:

  • Автоматический ретрай с экспоненциальным backoff'ом в internal/client:
    • Триггеры: HTTP 403 (CDN-WAF burst-shield), 429 (явный rate-limit), 5xx (временные глюки CDN), сетевые ошибки уровня TCP/timeout.
    • Расписание: 1s → 2s → 4s → 8s → 16s, до 5 попыток. Кеп 16s ниже, чем у GraphQL (30s), потому что воркер скачивания живёт в пуле параллельных воркеров — одна медленная попытка не должна стопорить всех.
  • JR-специфичный Rate ... GraphQL-ответ. Реактор иногда сигнализирует rate-limit не через HTTP 429, а через {"errors":[{"message":"Rate ..."}]} поверх HTTP 200. Теперь этот префикс детектируется в internal/graphql.Client.doOnce и направляется через тот же exponential-backoff retry-цикл (ErrRateLimited), что и реальный 429 — раньше он всплывал как фатальная ошибка «graphql error: Rate …».
  • Конфигурируемая минимальная пауза между запросами CDN. Новое поле в ⚙ → Сеть — «Мин. пауза между запросами CDN, мс». Это проактивный троттл поверх «потоков»: следующий GET к CDN не уходит, пока не прошло заданное число миллисекунд после предыдущего. Сериализация — sync.Mutex + atomic.Int64 для самой настройки (можно менять на лету).
    • 0 (по умолчанию) — старое поведение, «качаем так быстро, как тянут воркеры».
    • Эмпирическое значение — 200–400 мс. Чувствительный момент: настройка влияет на старт запроса, само скачивание идёт стримом параллельно, так что «4 потока × 300 мс» ≈ 13 файлов/сек, а не 4 в одну секунду.
    • Применяется горячо через тот же applyNetworkSettings, без перезапуска.

В сумме: даже если CDN начал выдавать серии 403, задача не разваливается — каждый запрос ретраится с растущим бэкоффом, плюс пользователь может поставить мин. паузу и проактивно снизить нагрузку.

Прочие доработки UI

  • Последняя ошибка скачивания — внутри карточки задачи в очереди, рядом со счётчиком ✖, теперь показывается последняя ошибка из per-file fetch (GET https://… HTTP 403, write error и т.п.). Полный текст в tooltip-е счётчика. Раньше было видно только общее число ошибок, и приходилось лезть в логи wails dev/exe.
  • Колонка имени в очереди не растягивается под длинной строкой ошибки. Технически таблица очереди переведена на table-layout: fixed, что приводит к работе text-overflow: ellipsis для длинных error-строк.
  • Карточка пресета показывает папку. Раньше папка отображалась только в tooltip-е всей строки, теперь рендерится как 📁 D:\joyreactor\art-cat-ears под основной строкой пресета.
  • Кнопка «✏️📁 Сменить папку» на каждом пресете — для самого частого фут-гана «забыл поменять папку перед сохранением пресета». Только меняет outDir, остальные поля не трогает.
  • Диалог «Сохранить пресет» — два поля, имя + папка с пикером. Папка наследуется из текущей формы, можно поменять до сохранения. Если выбранная папка уже привязана к другому пресету — выскакивает confirm-диалог.

Скачивание

Тот же набор файлов, что и в v1.0.3 — Windows portable / NSIS-setup (с embedded WebView2), macOS universal .app (для Apple Silicon и Intel в одном бинарнике), Linux .tar.gz.

Лицензия

MIT.

Full Changelog: v1.0.3...v1.0.4

Full Changelog: v1.0.3...v1.0.4