Поверх 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), и пишет его в каждую задачу одного поста. Параллельные воркеры не могут переставить картинки одного поста в разные подпапки, потому что подпапка уже зафиксирована в задаче.
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