- 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.
12 KiB
12 KiB
Сбор и хранилище
Читать перед правкой 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 12SetTextAsync— расширение из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отсекает это до всякого обращения к диску.