Архитектура
Эта страница объясняет, как проект устроен в текущем коде, а не в абстрактной идеальной версии.
Общая Картина
У репозитория три главных части:
frontend/для браузерного UIbackend/для API и пайплайна скачиванияdocs/для сайта документации на Docusaurus
Frontend
Frontend построен на Vite + React.
Точка входа
frontend/src/main.tsxмонтирует приложение и применяет MUI themefrontend/src/App.tsxвладеет основным экраном и верхнеуровневым state
Текущее поведение UI
Текущий интерфейс представляет собой одну landing-page страницу:
- sticky header
- hero с полем для URL
- встроенные карточки результата
- footer
После отправки URL:
App.tsxвызываетapi.extract- рендерится карточка медиа
- пользователь выбирает режим и формат
- пользователь нажимает
Подготовить файл - backend возвращает временный
download_url - frontend показывает явную ссылку
Скачать файл
Структура frontend
frontend/src/
├── App.tsx
├── main.tsx
├── components/
│ ├── DownloadButton.tsx
│ ├── FormatSelector.tsx
│ ├── MediaPreview.tsx
│ ├── PlatformBadges.tsx
│ └── URLInput.tsx
├── services/
│ └── api.ts
└── theme/
└── theme.ts
Важные frontend-модули
App.tsx
Хранит:
- состояние темы
- состояние popover со списком сервисов
- текущий URL
- loading/error state
- извлечённые данные медиа
- выбранный формат
- режим аудио/видео
Также внутри него лежит основная CSS-верстка текущего лендинга.
services/api.ts
Оборачивает backend API:
extract(url)download(request)getFormats(url)health()
FormatSelector.tsx
Реализует логику выбора формата:
- видео-режим использует реальные
format_id - аудио-режим использует заранее заданные выходные форматы вроде
mp3иwav
Это важно, потому что backend интерпретирует поле format по-разному в зависимости от audio_only.
Backend
Backend представляет собой Axum-приложение на порту 8080.
Router
Определён в backend/src/main.rs:
GET /GET /api/healthPOST /api/extractPOST /api/downloadGET /api/formats/downloads/*для отдачи временных файлов
Структура backend
backend/src/
├── main.rs
├── handlers/
│ ├── download.rs
│ ├── extract.rs
│ ├── formats.rs
│ └── mod.rs
├── extractors/
│ ├── bilibili.rs
│ ├── bluesky.rs
│ ├── dailymotion.rs
│ ├── facebook.rs
│ ├── generic.rs
│ ├── instagram.rs
│ ├── loom.rs
│ ├── newgrounds.rs
│ ├── odnoklassniki.rs
│ ├── pinterest.rs
│ ├── reddit.rs
│ ├── rutube.rs
│ ├── snapchat.rs
│ ├── soundcloud.rs
│ ├── streamable.rs
│ ├── tiktok.rs
│ ├── tumblr.rs
│ ├── twitch.rs
│ ├── twitter.rs
│ ├── vimeo.rs
│ ├── vk.rs
│ ├── youtube.rs
│ └── mod.rs
└── services/
├── downloader.rs
├── mod.rs
└── ytdlp.rs
Роли handler-ов
extract.rs
- валидирует URL
- выбирает extractor через
detect_platform - возвращает нормализованный ответ с метаданными и форматами
download.rs
- валидирует входной payload
- определяет, это видео-режим или аудио-режим
- передаёт либо видео
format_id, либо аудиоформат в downloader - возвращает временный
/downloads/...URL
formats.rs
Сейчас возвращает статический placeholder и не связан с реальным списком форматов от extractor-ов.
Слой Extractor-ов
Все extractor-ы реализуют MediaExtractor из backend/src/extractors/mod.rs.
Текущий trait:
pub trait MediaExtractor: Send + Sync {
fn detect(&self, url: &str) -> bool;
async fn extract_info(&self, url: &str) -> Result<MediaInfo>;
async fn get_download_url(&self, url: &str, format_id: &str) -> Result<String>;
}
Как выбирается extractor
detect_platform(url) проходит по зарегистрированным extractor-ам по порядку и возвращает первый, у которого detect() вернул true.
Важное следствие:
generic::GenericExtractorявляется fallback- он подходит для любого
http://илиhttps://URL - значит, он должен оставаться в конце списка
Пайплайн Скачивания
Основная логика распределена между двумя сервисами.
services/downloader.rs
Отвечает за:
- создание временной директории
- генерацию UUID-имён файлов
- выбор расширения выходного файла
- вызов
YtDlpService - проверку, что файл реально создан
services/ytdlp.rs
Отвечает за:
- запуск
yt-dlp --dump-jsonдля извлечения информации - разбор
formatsиз JSON - скачивание конкретного видеоформата
- извлечение аудио через
-x --audio-format
Селектор видеоформатов
Сейчас backend использует такую идею:
- если выбран формат, используется селектор вида
fmt+bestaudio/fmt/best - если формат не задан, используется fallback
bv*+ba/b
Это важно для платформ вроде Bilibili, где простой best может ломаться или выбирать не тот поток.
Жизненный Цикл Файла
Подготовленные файлы записываются в:
<system-temp>/media-downloader
В Docker-режиме compose монтирует:
./downloads -> /tmp/media-downloader
Код очистки есть в Downloader::cleanup_old_files(), но автоматически из main.rs он сейчас не запускается.
Текущие Ограничения
Это важные реальные ограничения:
- нет аутентификации
- нет настоящего rate limiting в текущем коде
GET /api/formatsпока заглушкаhealth.uptimeвсегда0- включён широкий
CORSдля всех origins - очистка файлов написана, но не запланирована как фоновая задача
Слои Развёртывания
Development
- frontend на
localhost:3000 - backend на
localhost:8080
Production
В репозитории есть:
docker-compose.ymlCaddyfile.example
Основные Docker-сервисы:
backendfrontendcaddy
Caddy должен проксировать browser-трафик во frontend, а /api/* запросы в backend.