Files
av-parser/docs/collecting.md
T
Leonid Pershin 0fa8fb89f6 Implement media rule management and enhance proxy handling
- Added methods to `IMediaStore` for loading, saving, and removing media rules, allowing users to manage rules for media items effectively.
- Updated `MediaFetcher` to utilize the new rule management system, integrating rule checks into the fetching process to handle geo-blocks and previously ruled items.
- Enhanced `ProxyPool` to support exclusion of proxies from specific countries during acquisition, improving the handling of geo-blocked content.
- Adjusted `FetchOptions` to include rules instead of tombstones, streamlining the decision-making process during media fetching.
- Updated UI components to support rule editing, providing users with a more interactive experience when managing media rules.

These changes improve the overall media collection process by allowing users to define rules for handling media items and enhancing the proxy management system for better content accessibility.
2026-08-15 15:10:55 +03:00

122 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Сбор и хранилище
Читать перед правкой `Collecting/**`, `Media/**`, `CollectViewModel` и `GalleryViewModel`.
## Гейт сбора
`CollectViewModel.RefreshProxyGate()`: **хотя бы один отмеченный** источник требует прокси
(`MediaSourceViewModel.NeedsProxy` = сетевой и без своего разрешения на прямое подключение) и
`LiveCount == 0`. Пересчитывается по событию пула (throttle 250 мс — пул дёргается на каждый исход
лизы) и при смене отметок. Подсветка в списке для этого не годится: она про то, что редактируют, а не
про то, что запускают.
- **Разрешение работать без прокси — настройка источника, а не приложения**
(`PatternSourceConfig.AllowDirectConnection`). Один хост может быть своим, где прокси бессмысленна,
а соседний — чужим, где прямое подключение это ровно то, чего пользователь избегал; общий тумблер
навязывал обоим разрешающий ответ. По умолчанию `false` — туда же приезжает старый
`sources.user.json`.
- **Один закрытый источник в прогоне закрывает весь прогон.** Разрешающий не может поручиться за
строгого: запрос, которого пользователь не хотел, всё равно ушёл бы с его адреса.
- **Второй экземпляр того же правила — `FetchOptions.RequireProxy`**, и он строится на источник в
`ProduceAsync`. Гейт гасит кнопку, а фетчер бросает `ProxyUnavailableException`; одного UI мало.
`CollectOptions.RequireProxy` по умолчанию `true`: молчание вызывающего — не разрешение.
- Баннер живёт под `x:Name="ProxyGateBanner"` и рендерится по-настоящему в `CollectViewTests`, потому
что мёртвый биндинг `IsVisible` не ломает ни одного VM-теста.
## Прогон по нескольким источникам
- **Отметка ≠ подсветка.** `MediaSourceViewModel.IsSelected` решает, что войдёт в прогон; выделение в
списке — что правят, удаляют и чистят. Отмеченный набор живёт в `AppSettings.CollectSourceIds`
строкой, а не списком: список сломал бы сравнение записи, и каждое сохранение выглядело бы
изменением.
- **Источники идут одновременно, а не по очереди.** Это не про пропускную способность: при нулевом
бюджете первый источник не заканчивается никогда, так что последовательный прогон был бы прогоном
по одному источнику со списком в руках. Слияние — `CollectViewModel.MergeAsync`; канал ограничен, а
`finally` обязан погасить продюсеров — оставить их писать в канал, который никто не читает, значит
подвесить их посреди записи в хранилище.
- **Глобальный лимит закачек делится на число источников, а не умножается**: каждый `CollectRunner`
поднимает своих воркеров, и пять источников по четыре — это двадцать соединений вместо четырёх.
- **`Limit = 0` — «пока не остановят».** В безлимитном режиме `PatternMediaSource` сбрасывает
множество виденных id по `SeenCapacity`: у прогона нет конца, значит и у множества не должно быть
роста. Настоящий дедуп держит журнал `seen_url`, а не оно.
## Источники
- **Несколько расширений — это один кандидат с запасными адресами**, а не несколько кандидатов. У id
одна картинка: три кандидата на три суффикса скачали бы попадание и потом пошли искать его
несуществующих близнецов, а счётчики посчитали бы одну находку за три. Порядок — инструкция:
`MediaFetcher.FetchAddressesAsync` идёт по `MediaCandidate.Addresses` до первого ответа.
- **Дальше по списку двигает только «здесь ничего нет»**: `Gone`, `NotMedia`, `TooSmall`,
`Placeholder`. Тайм-аут или отказ прокси — это отсутствие вердикта, а не вердикт: сжечь на нём
остальные суффиксы значит объявить id отсутствующим везде по одному сломанному соединению.
- **Пустое расширение — это `.jpg`, а не «без суффикса».** В редакторе это плейсхолдер, и
`PatternSourceConfig.NormaliseExtension` подставляет его в домене, потому что через `TryCreate`
проходят и форма, и загрузка `sources.user.json`. Голый `/{id}` почти всегда опечатка, которая
стоит целого прогона из 404.
## Живой журнал
- **Одна хронологическая лента вместо «результаты + ошибки»**: при переборе id почти всё промахи, и
смотрят на порядок, а не на две таблицы.
- **Пакетов мало, нужен ещё и тик.** `FlushInterval` (200 мс) существует потому, что на медленном
источнике буфер не добирает до `BatchSize` и страница выглядит зависшей. Буферы —
`ConcurrentQueue`, слив под `_flushGate`, иначе две гонки-выгрузки перемешают строки местами.
- **Строка хранит ключ и аргументы, а не готовое предложение** — смена языка посреди прогона иначе
оставит половину журнала по-английски. Литеральная половина (адрес, размер) не переводится никогда.
- **Прокси попытки живёт в `ParseError.Via`** и печатается в строке как `via {адрес}`: «сайт ответил
404» через подтверждённую прокси и через ту, с которой никто не разговаривал, — разные диагнозы, а
без этого поля разница невидима.
- **Адрес неудачи живёт в `ParseError.Subject`.** В `Message` его нет и быть не может: у шаблона
перевода фиксированные подстановки. Без него журнал говорит «сайт ответил 404» и не говорит, на
каком из десяти тысяч id.
- **Журнал ограничен `MaxLogEntries`** — у безлимитного прогона нет конца, а несрезанный список это
утечка памяти со скроллбаром.
- **Строки копируются**: множественное выделение, Ctrl+C и контекстное меню; текст строит
`CollectLogEntryViewModel.ToString()`, чтобы в буфер попало ровно то, что на экране, а не
повторный рендер, который тихо разъедется с шаблоном. Ctrl+C без выделения копирует весь журнал.
В Avalonia 12 `SetTextAsync` — расширение из `Avalonia.Input.Platform`, а не член `IClipboard`.
## Хранилище медиа
- **В `blobs/` попадает только дочитанное.** Загрузка идёт во временный файл в соседнем каталоге на
том же томе и продвигается переименованием. Обрыв оставляет `.part`, который подметает следующий
старт, а не обрезанную картинку, навсегда неотличимую от настоящей.
- **Тип — по сигнатуре, никогда по URL, расширению или `Content-Type`**: два из трёх выбирает тот,
кто отдаёт файл, и расширение на диске у пользователя не должно зависеть от чужого сервера.
- **`GIF89a` не доказывает анимацию**, и APNG не определяется по фиксированному префиксу: нужен обход
блоков (второй Image Descriptor) и чанков (`acTL` раньше первого `IDAT`). Ошибка тихая, поэтому
обходчики изолированы за `internal static` швами и проверяются на массивах байтов.
- **`ref_count` денормализован и пересчитывается, а не инкрементится**: апсерт `item` может заменить
строку, указывавшую на другой blob, и слепой `+1` уехал бы навсегда. `VerifyReferenceCountsAsync`
часть замысла, а не отладка.
- **Журнал `seen_url` переживает чистку**, иначе следующий прогон скачает заново ровно то, что
пользователь только что удалил. Терминальные исходы отделены от повторяемых: отказ описывает
момент, а не ресурс, и считать его окончательным значит терять контент на каждой сетевой икоте.
- **Троттл поднимается только сигналами хоста** (429/503 с `Retry-After`) и никогда не приводит к
ротации прокси. Флажка «повторить через другую прокси при 429» в настройках быть не должно.
- **Жёсткая ссылка — привилегия ФС, а не гарантия.** Откат на копию удваивает расход диска, поэтому
достигнутый режим пишется в `showcase_mode` и виден в UI.
- **Имя из `SuggestedName` враждебно**: остаётся только последний сегмент, разделители не переживают,
устройства Windows отодвигаются, расширение берётся из типа.
## Пути
- **Медиа лежит рядом с exe, настройки — в профиле.** Расхождение осознанное: конфигурация
пользовательская, а коллекция принадлежит установке и переезжает вместе с папкой.
- **`DefaultMediaDirectory()` проверяет запись пробным файлом**, а не только созданием каталога:
каталог создаётся и там, куда потом нельзя писать. При отказе — откат в профиль.
- **Смена каталога требует перезапуска и не переносит файлы.** `AppPaths` строится до контейнера
(медиа-корень читается из settings.json напрямую), так что на лету это не применить без
переподключения индекса, blob-хранилища и кэша миниатюр. Настройки честно это говорят и показывают
действующий путь.
## Отображение медиа
- **Миниатюры декодируются сразу в нужную ширину**, а не декодируются целиком и потом масштабируются.
На архиве это разница между «работает» и «кончилась память».
- **Кэш владеет своими `Bitmap` и удаляет их при вытеснении**, поэтому вызывающий не должен их
освобождать — и поэтому ёмкость кэша обязана заметно превышать страницу галереи: вытесненная
картинка, которая ещё на экране, освободилась бы под рендерером.
- **Видео не декодируется**, попытка была бы исключением на каждой плитке: `MediaKinds.IsImage`
отсекает это до всякого обращения к диску.