Skip to content

fix(media): send real video dimensions and duration from send_file - #366

Merged
chigwell merged 2 commits into
chigwell:mainfrom
axisrow:ao/sync-the-fork/fix-send-file-video-attributes
Oct 24, 2026
Merged

chigwell merged 2 commits into
chigwell:mainfrom
axisrow:ao/sync-the-fork/fix-send-file-video-attributes

Conversation

@axisrow

@axisrow axisrow commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #355 — videos sent through send_file arrived with DocumentAttributeVideo(w=1, h=1, duration=0) and no thumbnail: portrait clips squashed, durations 0:00.

Root cause

Telethon reads a video's width, height and duration only when hachoir is installed, and hachoir is GPL-2.0 while this project is Apache-2.0, so it can't be added. send_file passes no attributes, and Telethon's media-group branch never forwards caller attributes at all, so albums can't be fixed by passing kwargs either.

The fix

  • A native MP4/MOV container parser (stdlib only, struct + seeking, no whole-file reads): mvhd for duration, tkhd for track dimensions, applying the display matrix so a 90-degree-rotated phone video stored as 1920x1080 reports 1080x1920 — the rotation hachoir gets wrong. Any malformed structure returns None and the old behavior is kept (audio tracks are skipped by their zero dimensions; mvhd v0/v1 and 64-bit box sizes supported).
  • Single-file send_file: for videos with force_document=False, the parsed attribute is passed as attributes=[...]; Telethon overrides its own 1x1 default (attr_dict[type(a)] = a).
  • Albums containing at least one video are rebuilt as per-file InputMediaUploadedDocument / InputMediaUploadedPhoto and sent as a media group — the only way attributes survive. Image-only albums keep Telethon's own path untouched.
  • Non-MP4 videos (mkv/webm) fall back to the previous behavior.

Known limitation

Thumbnails are not attempted: extracting a frame needs an ffmpeg binary, which this project does not depend on, so the preview stays blank until the video is downloaded. Everything else from the issue (squashing, 0:00) is fixed for both single files and albums.

Tests

tests/test_send_file_video_meta.py: struct-built minimal MP4 fixtures — duration+dimensions, 90° rotation swap, identity matrix, 64-bit mvhd, five malformed/truncated containers returning None (including one that reads across box boundaries — the parser clamps reads to the box end); wire-in tests: single send_file passes the attribute, non-videos and force_document=True keep the old path, album with video builds per-file media (video item carries the attribute and nosound_video=True, images become InputMediaUploadedPhoto), image-only albums unchanged. Full suite: 1498 passed; black --check and flake8 --select=E501 clean.

🤖 Generated with Claude Code

Telethon fills a video's width, height and duration only when hachoir is
installed, and hachoir is GPL-2.0 while this project is Apache-2.0, so every
video went out as DocumentAttributeVideo(w=1, h=1, duration=0): portrait
clips arrived squashed, durations showed 0:00. Parse the MP4/MOV container
natively (mvhd for duration, tkhd for dimensions, applying the display
matrix so 90-degree-rotated phone videos report portrait dimensions) and
pass the attribute to Telethon, which overrides its own 1x1 default.
Albums containing video are rebuilt as per-file InputMedia because
Telethon's media-group branch never forwards caller attributes; image-only
albums keep Telethon's own path. Video thumbnails are not attempted (they
need a frame extractor); the preview stays blank until downloaded.

Co-Authored-By: Claude Code <[email protected]>

@axisrow axisrow left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #366 — real video dimensions and duration from send_file

Requesting changes: two bugs, each is an inline finding on telegram_mcp/tools/media.py.

Verified, no action needed:

  • Premise checks out against Telethon 1.44.0 source: _send_album calls _file_to_media without attributes, so caller attributes really are dropped in media groups; rebuilding video albums as per-file InputMedia is justified. Image-only albums keep the native path.
  • nosound_video=True matches what Telethon itself passes in _send_album and does not mute audible videos.
  • Single-file path: attributes=[DocumentAttributeVideo] is merged per-class by utils.get_attributes, so DocumentAttributeFilename survives.
  • Container walk is safe: top-level moov search, no infinite loop (size >= 8 enforced), tkhd v0/v1 offsets correct, 90/270-degree display-matrix swap correct; malformed inputs degrade to None and Telethon defaults.

Required changes:

  1. _parse_mvhd, version 1: mvhd v1 stores duration as a 64-bit field at offset 24; the >II unpack at offset 20 reads timescale plus only the high 32 bits of duration, which is zero for any real clip, so v1-mvhd videos still go out with duration=0 (the exact bug this PR fixes). Unpack >IQ from a 32-byte read with a len<32 check. The test helper _mvhd(version=1) packs the same wrong 28-byte layout and masks the bug; fix both together.
  2. _album_media_item: both InputMediaUploadedDocument branches omit DocumentAttributeFilename. Telethon always attaches one (utils.get_attributes), so in a rebuilt album the video and any non-image file arrive unnamed in clients. Add types.DocumentAttributeFilename(os.path.basename(path)) alongside the existing attributes.

Non-blocking: the commit message says the Telethon default without hachoir is duration=0; utils.get_attributes actually defaults to duration=1. Moot once finding 1 is fixed.

Verdict: changes_requested.

Comment thread telegram_mcp/tools/media.py Resolved
Comment thread telegram_mcp/tools/media.py Resolved
mvhd v1 stores duration as a 64-bit field at offset 24; the old >II
read at 20 grabbed the timescale plus only the high 32 bits of the
duration (zero for any real clip), so v1-mvhd videos still went out
with duration=0. Unpack >IQ from a full 32-byte payload, and build the
matching 32-byte v1 layout in the test helper that previously masked
this (the 64-bit test now uses a duration beyond 2**32).

Rebuilt album items also carry DocumentAttributeFilename like
Telethon's own path-based uploads: without it, mixed-album videos and
non-image files arrived unnamed.

Co-Authored-By: Claude Code <[email protected]>

@axisrow axisrow left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #366 — real video dimensions and duration from send_file (follow-up cd7a851)

Approving. Both findings from the previous round are fixed correctly:

  1. _parse_mvhd now unpacks >IQ from a 32-byte read with a len<32 check for version 1; the test fixture packs the real 32-byte v1 layout and uses duration 2**33 with timescale=1, so a parser reading only the high 32 bits would produce 0 and fail the assert; a truncated-v1 case was added to the malformed-input tests.
  2. _album_media_item attaches DocumentAttributeFilename in both InputMediaUploadedDocument branches, with tests asserting file_name for the video and for a non-image document in a mixed album.

Re-verified against the full diff to main: the single-file path and Telethon interplay are unchanged and still sound, image-only albums still take the native Telethon path, and nothing else moved in the follow-up commit.

Verdict: approved.

@chigwell
chigwell merged commit 2cb0c3d into chigwell:main Oct 24, 2026
4 checks passed
timurco added a commit to timurco/telegram-mcp that referenced this pull request Oct 25, 2026
… and custom_emojis

send_file and send_album accept the same parse_mode and custom_emojis as
send_message, reply_to_message and edit_message. With parse_mode='html' a
caption can carry <b>, <i>, links and <tg-emoji emoji-id="ID">, which
Telethon parses itself. Without HTML, custom_emojis marks the standalone
occurrences of each fallback emoji through the existing _plain_entities and
is sent as formatting_entities over the raw caption.

When neither argument is set nothing new reaches Telethon, so captions are
still parsed as Markdown, exactly as before. parse_mode='plain' sends the
caption verbatim, as in send_scheduled_message. custom_emojis together with
parse_mode, rich modes, and custom_emojis without a caption are refused
before anything is sent. Works for a single file and for a media group (the
caption sits on its first item), with force_document, topic_id and
schedule_date.

An album that contains video goes out as per-file InputMedia (chigwell#366); the
caption format is passed on that path as well.
@axisrow
axisrow deleted the ao/sync-the-fork/fix-send-file-video-attributes branch October 30, 2026 02:29
axisrow added a commit to axisrow/telegram-mcp that referenced this pull request Oct 30, 2026
Resolves the media.py conflict for the upstream side: it carries the same video-attributes work as fork #30 plus the chigwell#366 review follow-ups; the fork video code survives the auto-merge.

Co-Authored-By: Claude Code <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

send_file: videos arrive squashed into a square, with 0:00 duration and no preview

2 participants