Files
av-parser/docs/collecting.md
T
Leonid Pershin eb5061ee23 Refactor media source handling and update collection options
- Updated `IMediaSourceCatalog` to support user-added media sources, allowing dynamic editing and management of sources.
- Removed the `UrlListSource` class as its functionality is now integrated into the new catalog structure.
- Enhanced `CollectOptions` to default `RequireProxy` to true, ensuring stricter handling of proxy requirements.
- Improved error handling in `ParseError` to include a `Subject` field for better context on failures.
- Adjusted dependency injection to reflect changes in media source management, removing old source registrations.
- Introduced background proxy checks to ensure a more robust proxy pool management during collection processes.

These changes streamline the media collection process and improve the overall user experience by providing clearer error reporting and more flexible source management.
2026-08-15 14:20:06 +03:00

12 KiB
Raw Blame History

Сбор и хранилище

Читать перед правкой 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, а не оно.

Источники

  • Пустое расширение — это .jpg, а не «без суффикса». В редакторе это плейсхолдер, и PatternSourceConfig.NormaliseExtension подставляет его в домене, потому что через TryCreate проходят и форма, и загрузка sources.user.json. Голый /{id} почти всегда опечатка, которая стоит целого прогона из 404.

Живой журнал

  • Одна хронологическая лента вместо «результаты + ошибки»: при переборе id почти всё промахи, и смотрят на порядок, а не на две таблицы.
  • Пакетов мало, нужен ещё и тик. FlushInterval (200 мс) существует потому, что на медленном источнике буфер не добирает до BatchSize и страница выглядит зависшей. Буферы — ConcurrentQueue, слив под _flushGate, иначе две гонки-выгрузки перемешают строки местами.
  • Строка хранит ключ и аргументы, а не готовое предложение — смена языка посреди прогона иначе оставит половину журнала по-английски. Литеральная половина (адрес, размер) не переводится никогда.
  • Адрес неудачи живёт в 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 отсекает это до всякого обращения к диску.