Skip to content
Open
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
356231a
add: FileInspector -> FileInfo
Pankovea May 17, 2026
2b16f31
fix: ruff
Pankovea May 18, 2026
e2dbd7f
fix: NamedBytesIO
Pankovea May 18, 2026
1c4c299
add: tests for bot.get_file_info
Pankovea May 18, 2026
4e53713
fix: copilot review
Pankovea May 18, 2026
ee46a4c
fix: Сборка строки FileInfo.__str__
Pankovea May 18, 2026
6306369
fix: Сборка строки FileInfo.str (2)
Pankovea May 18, 2026
ba23730
Merge branch 'main' into feat_file_info
Pankovea May 18, 2026
fbb5f81
fix: some copilot comments
Pankovea May 18, 2026
9d432cd
fix: Оптимизированы сетевые запросы
Pankovea May 19, 2026
0e395ac
fix: Оправка headers c auth
Pankovea May 19, 2026
ca30328
refactor: Оптимизация
Pankovea May 19, 2026
9b9923d
fix: mp4 m4a sample_rate detection
Pankovea May 19, 2026
487056c
refactor: Отказ от планирования загрузки. Парсеры рулят.
Pankovea May 19, 2026
ee82d03
fix: Опечатка в комментарии: «Провеирть» → «Проверить».
Pankovea May 25, 2026
aec25b2
Fix typo in file_inspector.py documentation
Pankovea May 25, 2026
8e30263
fix: Опечатка в сообщении исключения: «отсутвует» → «отсутствует».
Pankovea May 25, 2026
8eedc5d
fix: Неверное положение скобок в расчёте file_size
Pankovea May 25, 2026
ebff36e
fix: Проверка на утечку авторизации смотреть так же в self.session
Pankovea May 25, 2026
53761c2
fix: max_total, ограничивал только внутри одного вызова
Pankovea May 25, 2026
59172b2
fix: tests
Pankovea May 25, 2026
6819c2c
fix: ruff
Pankovea May 25, 2026
7f580f8
fix: tests
Pankovea May 25, 2026
cc07b14
fix: tests
Pankovea May 25, 2026
dfa714f
fix: tests
Pankovea May 25, 2026
28d3582
fix: tests
Pankovea May 25, 2026
791a0dd
fix: tests
Pankovea May 25, 2026
e299fb2
fix: Оптимизация _webp_parse
Pankovea Jun 12, 2026
1af49e7
add: tests to test_file_info.py
Pankovea Jun 12, 2026
d8d3763
Merge commit '28b7c446bf6191c0e19a601055fd21d91b393de9' into feat_fil…
Pankovea Jun 12, 2026
0e0711a
fix: tests
Pankovea Jun 12, 2026
9f551f2
fix: ruff
Pankovea Jun 12, 2026
cf55c4b
add: test_file_info_class.py,
Pankovea Jun 12, 2026
025e87c
add: FileInspector to utils\__init__.py
Pankovea Jun 13, 2026
2f3a4d9
add: Документация и примеры
Pankovea Jun 13, 2026
d47f736
fix: circular import
Pankovea Jun 13, 2026
3bb8793
Merge branch 'main' into feat_file_info
Pankovea Jun 18, 2026
3b9d974
Merge branch 'main' into feat_file_info
Pankovea Jul 1, 2026
7a4ba1f
fix: anyio -> aiofiles чтобы не добавлять зависимостей
Pankovea Jul 1, 2026
14c3f26
remove: FileInfo.__eq__
Pankovea Jul 1, 2026
8a0cfa0
fix: RangeDownloader._expand_head теперь обновляет _downloaded
Pankovea Jul 1, 2026
af35636
fix: RuntimeWarning на мок-сессиях
Pankovea Jul 1, 2026
f6ee59a
ruff
Pankovea Jul 1, 2026
7a88ce6
add: Удобная обёртка над :class:`File_inspector
Pankovea Jul 1, 2026
dcde4ac
remove: Bot.get_file_info
Pankovea Jul 5, 2026
0ec337f
add: UrlStr
Pankovea Jul 7, 2026
d011e55
refactor: RangeReader.meta: FileMeta
Pankovea Jul 7, 2026
6059fab
add: FileInspector.full_file_save()
Pankovea Jul 7, 2026
89f124e
ruff 901 TooComplex для file_inspector в pyproject.toml
Pankovea Jul 8, 2026
404e54a
ruff
Pankovea Jul 8, 2026
4b5ba34
ruff
Pankovea Jul 8, 2026
e76a022
add: UrlStr.full_file_save(file_path)
Pankovea Jul 8, 2026
52b7a72
imports: inspect_bytes, inspect_file, inspect_url
Pankovea Jul 8, 2026
a9c8293
fix: Примеры для FileInfo
Pankovea Jul 8, 2026
551d73d
fix: tests
Pankovea Jul 8, 2026
4cffcd4
ruff
Pankovea Jul 8, 2026
271ed7e
add: HLS / M3U parsing
Pankovea Jul 8, 2026
b7c6301
ruff
Pankovea Jul 8, 2026
520bed1
fix: Орфография и докстринги
Pankovea Jul 10, 2026
c1a1eea
fix: FileInfo.__str__ проверка на наличие параметра через is not None
Pankovea Jul 10, 2026
4da6289
fix: Вынесение FileInspector во внешний иморт
Pankovea Jul 13, 2026
05547a7
fix: миграция FileInspector → MediaProbe, исправление доков
Pankovea Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Здесь собраны практические примеры использования MaxAPI для различных задач.

Смотри так же примеры в (maxapi/examples)[https://github.com/love-apples/maxapi/tree/main/examples]
Comment thread
Pankovea marked this conversation as resolved.
Outdated

## Эхо-бот

Простейший пример бота, который повторяет все текстовые сообщения:
Expand Down Expand Up @@ -993,6 +995,108 @@ if __name__ == "__main__":
asyncio.run(main())
```

## Инспекция файлов

Для получения расширенной метаинформации о файле (формат, размер, MIME-тип, размеры
изображения, длительность видео) без полной загрузки файла используется `UrlStr.get_info()`.
Скачиваются только необходимоей количество байт для определения параметров медиа
Comment thread
Pankovea marked this conversation as resolved.
Outdated
с начала файла и иногда с конца.

```python
import asyncio
import logging

from maxapi import Bot, Dispatcher, F
from maxapi.filters.command import Command
from maxapi.types import MessageCreated

logging.basicConfig(level=logging.INFO)

bot = Bot()
dp = Dispatcher()


@dp.message_created(Command('info'))
async def cmd_info(event: MessageCreated):
replied = event.message.link.message if event.message.link else None
if not replied or not replied.attachments:
await event.message.answer('Ответьте на сообщение с файлом.')
return

first = replied.attachments[0]
url = first.url if hasattr(first, 'url') else None
if not url:
await event.message.answer('Вложение не содержит URL.')
return

# get_info() всегда возвращает FileInfo (в т.ч. при ошибке)
info = await url.get_info()

if info.status == "error":
await event.message.answer(f'Ошибка: {info.parse_note}')
return

# str(info) — человекочитаемый вывод
await event.message.answer(
f"{info}"
f"\nДля определения параметров файла было скачано {url.inspector.downloaded_human}"
)

# Докачка файла через существующее соединение
# Напишите собственные условия для скачивания
if info.mime_type.startswith("video") and info.height > 320:
await event.message.answer("Файл подходит. Сохраняю...")
path = await url.full_file_save("files/downloads_directory")
await event.message.answer(f'Сохранён в {path}')


async def main():
await dp.start_polling(bot)


if __name__ == '__main__':
asyncio.run(main())
```

Если у вас есть URL как обычная строка (не из вложения), используйте
свободную функцию `inspect_url`:

```python
from maxapi.utils import inspect_url

info = await inspect_url('https://example.com/video.mp4', timeout=10)
print(info.format) # "MP4"
print(info.width) # 1920
print(info.height) # 1080
print(info.duration) # 10.0
print(info.status) # "ok"
print(info)
# Имя файла: video.mp4
# Размер: 12,5 Мб
# Формат: MP4
# Размеры: 1920х1080 пикс
# Длительность: 10 сек
# Частота кадров: 25 к/с
# Аудио: 48000 Гц
# Битрейт (средний): 10240 кбит/с

# Для аудио файлов моджет быть:
# Битрейт (номинальный): 320 кбит/с"
Comment thread
Pankovea marked this conversation as resolved.
Outdated
```
Аналогичным способом можно проанализировать байты и файлы с диска.
Например если вы уже скачали файл через bot.download_file(url)
или bot.download_bytes(url)

```python
from maxapi.utils import inspect_bytes, inspect_file

info = await inspect_bytes(bytes_or_bytes_io)
print(info)
info = await inspect_file(file_path_str_or_Path)
print(info)
```


## Webhook

### Высокоуровневый подход
Expand Down
165 changes: 165 additions & 0 deletions docs/guides/file_inspector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Получение метаинформации о медиафайлах

Библиотека `maxapi` позволяет извлекать метаданные медиафайлов (формат, размеры, длительность, битрейт) по URL **без полной загрузки**, локальному пути или байтам в памяти.
Анализирует сигнатуры и заголовки в первых и последних байтах файла. Скачивает минимум данных, докачивает
только если не хватило.

Это полезно для:

- Быстрой проверки типа файла перед скачиванием.
- Получения размеров изображений/видео для отображения в UI.
- Определения длительности аудио/видео для превью.

## Поддерживаемые форматы

FileInspector распознает метаданные для популярных медиаформатов:
* Изображения: JPEG, PNG, GIF, WebP (VP8/VP8L/VP8X)
* Видео: MP4/MOV, AVI, MKV, WEBM, OGV
* Аудио: MP3, AAC, WAV, WMA, FLAC, OGG, M4A

Для каждого формата извлекаются поля (если доступно):
width, height, duration, fps, sample_rate, bitrate

## Быстрый старт: `bot.get_file_info()`

Самый простой способ — использовать метод `bot.get_file_info()`, который принимает URL файла и возвращает `FileInfo`:

```python
from maxapi import Bot

bot = Bot(token="YOUR_TOKEN")

# Получаем метаинформацию о файле по URL
info = await bot.get_file_info(
"https://example.com/video.mp4",
timeout=10, # таймаут в секундах (опционально)
)

print(info)

# Или отдельно по интересующим полям
print(f"Формат: {info.format}")
print(f"Размеры: {info.width}x{info.height}")
print(f"Длительность: {info.duration} сек")
print(f"Статус: {info.status}") # ok, partial или error
if info.status != "ok":
print(f" Комментарий парсера: {info.parse_note}")
```

## Анализ файла на диске или байт в памяти

```python
from maxapi.utils import FileInspector
inspector = FileInspector()
file_info = await inspector.inspect_file("/path/to/video.avi")
file_info = await inspector.inspect_bytes(downloaded_bytes)
# Следующий метод интегрирован в bot.get_file_info(url)
file_info = await inspector.inspect_url("https://example.com/photo.jpg")
```

FileInspector можно использовать повторно для других файлов.
При этом он помнит последние скачанные данные и последний FileInfo:

```python
if inspector.last_file_info:
print(inspector.last_file_info)
print("Скачано начало файла", len(inspector.last_head), "байт")
if inspector.last_tail:
print("Скачано конца файла", len(inspector.last_tail), "байт")
```

## Обработка статусов

Метод возвращает FileInfo со статусом:
* `ok` — все ключевые метаданные успешно извлечены.
* `partial` — часть данных получена, но чего-то не хватает (например, длительность для MP4 с moov в конце файла).
* `error` — произошла ошибка (сеть, HTML-страница вместо файла) и не удалось определить даже размер файла.

```python
info = await bot.get_file_info(url)

if info.status == "ok":
print(f"Полные метаданные: {info.format}, {info.width}x{info.height}")
elif info.status == "partial":
print(f"Частичные данные: {info.format}, примечание: {info.parse_note}")
else:
print(f"Ошибка: {info.parse_note}")
```

## Как это работает?

FileInspector использует частичную загрузку:
* HEAD-запрос для получения Content-Type и Content-Length.
Comment thread
Pankovea marked this conversation as resolved.
Outdated
* Скачивание хвоста 64 КБ — для форматов, где метаданные в конце (MP4 с moov в конце,
OGG с длительностью в последней грануле).
* Чтение начала файла от 4 до 256 КБ в зависимости от формата.

Если сервер не поддерживает Range-запросы, FileInspector адаптируется и работает с тем, что есть,
возвращая статус partial при невозможности определить некоторые поля.

## Безопасность и авторизация

Если вы передаете aiohttp.ClientSession с заголовками авторизации (Authorization, Cookie), они не будут отправлены на сторонние домены по умолчанию. Это защита от утечки токенов.
Чтобы разрешить отправку авторизации на внешний URL:

```python
from maxapi.utils import FileInspector
inspector = FileInspector()
info = await inspector.inspect_url(
"https://external.com/private.mp4",
session=session_with_auth,
allow_external_auth=True, # явно разрешаем
)
```

Доверенные домены (**oneme.ru**, **okcdn.ru**) всегда принимают авторизацию без этого флага.

## Пример в боте: команда /info

Добавим команду, которая показывает метаинформацию о файле из reply-сообщения:

```python
import asyncio
from maxapi import Bot, Dispatcher
from maxapi.types import Message

bot = Bot(token="ваш_токен")
dp = Dispatcher()

@dp.message_created(commands=["info"])
async def cmd_info(event: MessageCreated):
replied_body = event.message.link.message if event.message.link else None
if not replied_body or not replied_body.attachments:
await event.message.answer("ℹ️ Ответьте этой командой на сообщение с файлом.")
return

first_url = None
# Получаем URL вложений до первого успеха
if replied_body.attachments:
for att in replied_body.attachments:
if hasattr(att, "url"):
file_info = await bot.get_file_info(att.url)
if file_info.status == "ok":
# Собрать отдельные интересующие поля
# text = f"Формат: {file_info.format}\n"
# if file_info.width and file_info.height:
# text += f"Размеры: {file_info.width}x{file_info.height}\n"
# if file_info.duration:
# text += f"Длительность: {file_info.duration} сек\n"
# if file_info.sample_rate:
# text += f"Частота сэмплов: {file_info.sample_rate} Гц\n"
# text += f"Статус: {file_info.status}"
# Или просто:
text = str(file_info)
await event.message.answer(text)
return

await event.message.answer("Вложение не найдено")
return


if __name__ == '__main__':
asyncio.run(dp.start_polling(bot))
```

Более подробный пример (05_media_bot.py)[https://github.com/love-apples/maxapi/blob/main/examples/05_media_bot.py]
Comment thread
Pankovea marked this conversation as resolved.
Outdated
8 changes: 8 additions & 0 deletions docs/utils/file_inspector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# File Inspector

::: maxapi.utils.file_inspector
options:
show_root_heading: true
members_order: source
filters:
- "!^_"
Loading
Loading