File size: 9,079 Bytes
5f400f8
 
 
 
 
 
 
 
 
 
 
 
6c3a31d
5f400f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b737d7a
5f400f8
b737d7a
 
 
5f400f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
"""/ethumb: ask for a file or video, send back its thumbnail."""
import io
import shutil
import time
from pathlib import Path
from types import SimpleNamespace

from PIL import Image
from telethon import types

from bot import app as appmod

from .appkit import OWNER, STRANGER, auto_thumb, click, data_of, jpeg, say, send_doc
from .conftest import needs_ffmpeg
from .tgfakes import FakeEvent, FakeMessage, dispatch, document_message

REAL_THUMB = [SimpleNamespace(type="m")]  # stands in for a PhotoSize
STRIPPED = [types.PhotoStrippedSize(type="i", bytes=b"\x01\x14\x28abc")]


def photos_sent(ctx):
    return [(entity, file, kw) for entity, file, kw in ctx.client.files if isinstance(file, io.BytesIO)]


def temp_leftovers(ctx):
    root = Path(ctx.cfg.temp_dir)
    return list(root.glob("ethumb*")) if root.exists() else []


def serve(monkeypatch, media_dir, name="sample.mkv"):
    """Make transfer.download copy a real sample file; returns the list of calls."""
    calls = []

    async def fake_download(client, message, dest, **kw):
        calls.append(kw)
        shutil.copy(media_dir / name, dest)

    monkeypatch.setattr(appmod.transfer, "download", fake_download)
    return calls


# ---------------------------------------------------------------------------
# the flow
# ---------------------------------------------------------------------------


async def test_ethumb_asks_for_a_file(ctx):
    ev = await say(ctx, OWNER, "/ethumb")
    assert "EXTRACT THUMBNAIL" in ev.texts[0] and "one file at a time" in ev.texts[0] and "/cancel" in ev.texts[0]
    assert data_of(ev.out[0][1]) == ["et:stop"]
    mode = ctx.app.modes[OWNER]
    assert mode.kind == "ethumb" and mode.expires > time.time() + 500


async def test_the_files_own_telegram_thumbnail_is_sent_when_it_has_one(ctx, monkeypatch):
    async def no_download(*a, **kw):
        raise AssertionError("nothing needs downloading when Telegram already has a preview")

    monkeypatch.setattr(appmod.transfer, "download", no_download)
    await say(ctx, OWNER, "/ethumb")
    ctx.client.thumb_data[60] = jpeg((320, 180))
    ev = await send_doc(ctx, OWNER, "Episode 01.mp4", thumbs=REAL_THUMB, mid=60)
    assert ev.texts == ["🖼 Extracting the thumbnail…"]
    (entity, file, kw), = photos_sent(ctx)
    assert entity == OWNER and file.name == "thumbnail.jpg" and Image.open(io.BytesIO(file.getvalue())).size == (320, 180)
    assert "Thumbnail of <b>Episode 01.mp4</b>" in kw["caption"] and kw["parse_mode"] == "html"
    assert (OWNER, [1001]) in ctx.client.deleted  # the "Extracting…" message goes
    assert OWNER not in ctx.app.modes and ctx.client.downloads == [(60, bytes, -1)]


@needs_ffmpeg
async def test_without_a_telegram_thumbnail_a_frame_is_grabbed_from_the_video(ctx, media_dir, monkeypatch):
    calls = serve(monkeypatch, media_dir)
    await say(ctx, OWNER, "/ethumb")
    await send_doc(ctx, OWNER, "Show - 05.mkv", size=(media_dir / "sample.mkv").stat().st_size, mid=61)
    (_e, file, kw), = photos_sent(ctx)
    img = Image.open(io.BytesIO(file.getvalue()))
    assert img.format == "JPEG" and img.size == (320, 240)
    assert len(calls) == 1 and calls[0]["max_bytes"] == appmod.TRACKS_PROBE_BYTES
    assert "Show - 05.mkv" in kw["caption"] and (OWNER, [1001]) in ctx.client.deleted
    assert temp_leftovers(ctx) == []


@needs_ffmpeg
async def test_the_whole_file_is_fetched_when_the_start_has_no_picture(ctx, media_dir, monkeypatch):
    calls = []

    async def fake_download(client, message, dest, **kw):
        calls.append(kw)
        if "max_bytes" in kw:
            Path(dest).write_bytes(b"\x00" * 4096)  # an mp4 whose index is at the end
        else:
            shutil.copy(media_dir / "sample.mp4", dest)

    monkeypatch.setattr(appmod.transfer, "download", fake_download)
    await say(ctx, OWNER, "/ethumb")
    await send_doc(ctx, OWNER, "Movie.mp4", size=40 * 1024 * 1024, mid=62)
    assert len(calls) == 2 and "max_bytes" not in calls[1]
    assert len(photos_sent(ctx)) == 1 and temp_leftovers(ctx) == []


@needs_ffmpeg
async def test_a_file_with_no_picture_anywhere_gets_an_honest_answer(ctx, monkeypatch):
    async def junk(client, message, dest, **kw):
        Path(dest).write_bytes(b"\x00" * 4096)

    monkeypatch.setattr(appmod.transfer, "download", junk)
    for size in (2 * 1024 * 1024, 900 * 1024 * 1024):  # small / too big to fetch whole
        ctx.client.edited.clear()
        await say(ctx, OWNER, "/ethumb")
        await send_doc(ctx, OWNER, "Broken.mkv", size=size, mid=63)
        assert "couldn't find a thumbnail" in ctx.client.edited[-1][1], size
    assert photos_sent(ctx) == [] and temp_leftovers(ctx) == []


async def test_files_that_cannot_have_a_frame_are_not_downloaded(ctx, monkeypatch):
    async def no_download(*a, **kw):
        raise AssertionError("a zip has no picture to grab")

    monkeypatch.setattr(appmod.transfer, "download", no_download)
    await say(ctx, OWNER, "/ethumb")
    await send_doc(ctx, OWNER, "archive.zip", size=5 * 1024 * 1024, video=False, mime="application/zip", mid=64)
    assert "couldn't find a thumbnail" in ctx.client.edited[-1][1] and photos_sent(ctx) == []


@needs_ffmpeg
async def test_the_tiny_blurred_preview_is_only_the_last_resort(ctx, media_dir, monkeypatch):
    serve(monkeypatch, media_dir)
    await say(ctx, OWNER, "/ethumb")
    ctx.client.thumb_data[65] = b"BLURRY-PREVIEW"
    await send_doc(ctx, OWNER, "Show.mkv", size=(media_dir / "sample.mkv").stat().st_size, thumbs=STRIPPED, mid=65)
    (_e, file, _kw), = photos_sent(ctx)
    assert file.getvalue() != b"BLURRY-PREVIEW" and Image.open(io.BytesIO(file.getvalue())).size == (320, 240)  # a real frame wins

    async def junk(client, message, dest, **kw):
        Path(dest).write_bytes(b"\x00" * 4096)

    monkeypatch.setattr(appmod.transfer, "download", junk)
    await say(ctx, OWNER, "/ethumb")
    await send_doc(ctx, OWNER, "Show2.mkv", size=2 * 1024 * 1024, thumbs=STRIPPED, mid=65)
    assert photos_sent(ctx)[-1][1].getvalue() == b"BLURRY-PREVIEW"  # nothing better could be made


async def test_ethumb_as_a_reply_works_at_once_without_asking(ctx):
    ctx.client.thumb_data[66] = jpeg((320, 180))
    reply = document_message("Episode.mp4", mid=66, thumbs=REAL_THUMB)
    await say(ctx, OWNER, "/ethumb", reply=reply, mid=67)
    assert len(photos_sent(ctx)) == 1 and OWNER not in ctx.app.modes


async def test_it_handles_one_file_per_command(ctx):
    ctx.client.thumb_data[70] = jpeg()
    await say(ctx, OWNER, "/ethumb")
    await send_doc(ctx, OWNER, "One.mp4", thumbs=REAL_THUMB, mid=70)
    extra = await send_doc(ctx, OWNER, "Two.mp4", thumbs=REAL_THUMB, mid=71)  # right after: a hint, not an auto-rename
    assert "/ethumb handles one file at a time" in extra.texts[0] and ctx.app.queue.waiting == 0
    assert (await send_doc(ctx, OWNER, "Three.mp4", thumbs=REAL_THUMB, mid=72)).out == []  # further ones: silence
    assert len(photos_sent(ctx)) == 1
    ctx.app._recent[OWNER][1] -= appmod.ONE_SHOT_GRACE + 1
    later = await send_doc(ctx, OWNER, "Four.mp4", mid=73)
    assert "Queued" in later.texts[0]  # normal auto-rename is back


async def test_a_photo_while_waiting_is_ignored_and_the_wait_goes_on(ctx):
    await say(ctx, OWNER, "/ethumb")
    ev = FakeEvent(OWNER, message=FakeMessage("", photo=True, mid=80), photo_bytes=jpeg())
    await dispatch(ctx.client, ev)
    assert ev.out == [] and await auto_thumb(ctx) is None and ctx.app.modes[OWNER].kind == "ethumb"  # a photo is nobody's thumbnail


async def test_cancel_and_the_stop_button_end_it(ctx):
    await say(ctx, OWNER, "/ethumb")
    ev = await say(ctx, OWNER, "/cancel")
    assert "Cancelled" in ev.texts[0] and OWNER not in ctx.app.modes
    ev = await send_doc(ctx, OWNER, "A.mkv", mid=90)
    assert "Queued" in ev.texts[0]

    await say(ctx, OWNER, "/ethumb")
    cb = await click(ctx, OWNER, "et:stop")
    assert "Stopped" in cb.edits[-1][0] and OWNER not in ctx.app.modes


async def test_it_expires(ctx):
    await say(ctx, OWNER, "/ethumb")
    ctx.app.modes[OWNER].expires = time.time() - 1
    ev = await send_doc(ctx, OWNER, "A.mkv", mid=91)
    assert "Queued" in ev.texts[0]


async def test_strangers_cannot_use_it(ctx):
    ev = await say(ctx, STRANGER, "/ethumb")
    assert ev.out == [] and STRANGER not in ctx.app.modes


async def test_starting_it_ends_a_sequence_and_says_so(ctx):
    await say(ctx, OWNER, "/sequence")
    await send_doc(ctx, OWNER, "Show - 01.mkv", mid=92)
    ev = await say(ctx, OWNER, "/ethumb")
    assert "1 file(s) was discarded" in ev.texts[0] and ctx.app.modes[OWNER].kind == "ethumb"


async def test_a_failing_extraction_is_reported_not_raised(ctx, monkeypatch):
    async def boom(*a, **kw):
        raise RuntimeError("network down")

    monkeypatch.setattr(appmod.transfer, "download", boom)
    await say(ctx, OWNER, "/ethumb")
    ev = await send_doc(ctx, OWNER, "A.mkv", mid=93)
    assert ev.texts == ["🖼 Extracting the thumbnail…"] and "couldn't find a thumbnail" in ctx.client.edited[-1][1]
    assert temp_leftovers(ctx) == []