File size: 21,219 Bytes
58f00c9
 
 
 
 
 
 
84e63a1
58f00c9
 
 
 
 
 
 
b737d7a
58f00c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84e63a1
58f00c9
 
 
 
 
 
 
 
84e63a1
58f00c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4c871b
58f00c9
 
 
 
 
 
 
 
c3c6394
 
 
 
 
 
 
 
 
58f00c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84e63a1
58f00c9
 
 
 
 
 
 
 
6c3a31d
 
b737d7a
6c3a31d
 
 
 
 
 
 
58f00c9
 
 
6c3a31d
58f00c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84e63a1
58f00c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84e63a1
 
 
 
 
 
 
 
 
 
 
 
 
58f00c9
84e63a1
58f00c9
 
 
84e63a1
58f00c9
 
84e63a1
 
 
58f00c9
 
 
84e63a1
 
 
 
 
 
 
 
58f00c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5f400f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c3a31d
 
 
5f400f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c3a31d
 
5f400f8
 
 
 
 
6c3a31d
5f400f8
 
 
6c3a31d
 
5f400f8
 
 
 
 
 
 
 
6c3a31d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5f400f8
 
6c3a31d
5f400f8
 
 
 
 
 
529b973
6c3a31d
529b973
 
 
 
 
 
6c3a31d
529b973
 
 
 
 
 
 
 
 
 
 
6c3a31d
 
529b973
 
 
 
 
 
 
 
 
6c3a31d
 
529b973
6c3a31d
529b973
 
5f400f8
 
 
6c3a31d
 
 
 
 
 
 
 
 
 
 
 
 
5f400f8
6c3a31d
5f400f8
 
 
 
 
6c3a31d
5f400f8
 
a00abaf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
import shutil
from pathlib import Path
from types import SimpleNamespace

import pytest
from telethon import errors, types

from bot import errorlog
from bot.config import load_config
from bot.db import Database
from bot.media import probe
from bot.parser import DEFAULT_TEMPLATE, compute_new_name
from bot.pipeline import Renamer, decide_send_type, guess_mime, original_name_of, source_ext
from bot.queue import Job

from .appkit import ensure_auto
from .conftest import needs_ffmpeg


class FakeMessage:
    def __init__(self, name, size, *, video=True, audio=False, mime="video/x-matroska", msg_id=7, ext=None):
        self.id = msg_id
        self.file = SimpleNamespace(name=name, size=size, ext=ext if ext is not None else Path(name or "").suffix, mime_type=mime)
        self.video = object() if video else None
        self.audio = object() if audio else None
        self.document = object()


class FakeClient:
    def __init__(self, source: Path, message: FakeMessage):
        self.source = source
        self.message = message
        self.sent = []
        self.edits = []
        self.uploads = {}
        self.download_error = None
        self.send_errors = []

    async def get_messages(self, chat_id, ids):
        return self.message

    async def download_media(self, message, file, progress_callback):
        progress_callback(1, 2)
        if self.download_error:
            raise self.download_error
        shutil.copy(self.source, file)
        progress_callback(2, 2)

    async def upload_file(self, path, file_name, progress_callback):
        progress_callback(1, 1)
        keep = Path(path).with_name("uploaded_copy_" + Path(path).name)
        keep = Path(self.source).parent / ("upload-" + str(len(self.uploads)) + Path(path).suffix)
        shutil.copy(path, keep)
        self.uploads[len(self.uploads)] = keep
        return types.InputFileBig(id=len(self.uploads), parts=1, name=file_name)

    async def send_file(self, target, input_file, **kwargs):
        if self.send_errors:
            raise self.send_errors.pop(0)
        self.sent.append((target, input_file, kwargs))

    async def edit_message(self, message, text, buttons=None, parse_mode=None):
        self.edits.append((text, buttons))


@pytest.fixture
async def env(tmp_path, media_dir):
    cfg = load_config({
        "API_ID": "1", "API_HASH": "h", "BOT_TOKEN": "1:t", "OWNER_ID": "42",
        "TEMP_DIR": str(tmp_path / "work"), "FAST_TRANSFER": "0", "PROGRESS_INTERVAL": "2",
    })
    db = Database(f"sqlite+aiosqlite:///{tmp_path}/p.db")
    await db.init()
    ns = SimpleNamespace(cfg=cfg, db=db, media_dir=media_dir, tmp=tmp_path)
    yield ns
    await db.close()


def make(env, name="[SubsPlease] Tokyo Revengers - 05 (1080p) [ABCD1234].mkv", src="sample.mkv", **msg_kwargs):
    source = env.media_dir / src
    message = FakeMessage(name, source.stat().st_size, **msg_kwargs)
    client = FakeClient(source, message)
    renamer = Renamer(client, env.cfg, env.db)
    new_name, _ = compute_new_name(template=DEFAULT_TEMPLATE, mode="filename", original_name=name, original_ext=Path(name).suffix)
    job = Job(id=1, user_id=42, chat_id=42, msg_id=7, file_name=name, size=source.stat().st_size, new_name=new_name,
              status_msg=SimpleNamespace(id=99))
    return client, renamer, job


@needs_ffmpeg
async def test_plain_rename_sends_video_with_thumbnail(env):
    client, renamer, job = make(env)
    await renamer.process(job)

    assert job.new_name == "[S01E05] Tokyo Revengers [1080p].mkv"
    (target, input_file, kw), = client.sent
    assert target == 42 and kw["reply_to"] == 7
    by_type = {type(a): a for a in kw["attributes"]}
    assert by_type[types.DocumentAttributeFilename].file_name == job.new_name
    video = by_type[types.DocumentAttributeVideo]
    assert (video.w, video.h) == (320, 240) and 1 < video.duration < 3 and video.supports_streaming
    assert kw["force_document"] is False and kw["mime_type"] == "video/x-matroska"
    assert kw["thumb"][:2] == b"\xff\xd8"  # generated from a video frame
    assert kw["caption"] == "<b>[S01E05] Tokyo Revengers [1080p]</b>" and kw["parse_mode"] == "html"  # bold, by default
    assert input_file.name == job.new_name
    assert client.uploads[0].read_bytes() == (env.media_dir / "sample.mkv").read_bytes()  # untouched without metadata
    assert "Done" in client.edits[-1][0] and client.edits[-1][1] is None
    assert job.state == "done"
    assert (await env.db.totals())[0] == 1
    assert not (Path(env.cfg.temp_dir) / "job1").exists()  # temp files cleaned


@needs_ffmpeg
async def test_notes_from_queueing_appear_in_the_done_message(env):
    client, renamer, job = make(env)
    job.notes = ["Couldn't find {episode} in the file name, so it is left empty."]
    await renamer.process(job)
    assert job.state == "done"
    assert "⚠️ Couldn't find {episode} in the file name" in client.edits[-1][0]


@needs_ffmpeg
async def test_metadata_and_track_selection_are_applied_before_upload(env):
    await env.db.update_settings(
        42, metadata_on=True, keep_audio=["english", "japanese"], keep_subtitle=["2"],
        meta={"title": "Tokyo Revengers", "audio": "{lang}", "subtitle": "English"},
    )
    client, renamer, job = make(env)
    await renamer.process(job)
    out = await probe(client.uploads[0])
    assert [s.lang for s in out.audio] == ["eng", "jpn"]
    assert [s.lang for s in out.subtitles] == ["eng"]
    assert out.tags.get("title") == "Tokyo Revengers"
    assert [s.title for s in out.audio] == ["English", "Japanese"]
    assert client.sent and job.state == "done"


@needs_ffmpeg
async def test_document_file_type_is_forced(env):
    await env.db.update_settings(42, file_type="document")
    client, renamer, job = make(env)
    await renamer.process(job)
    (_t, _f, kw), = client.sent
    assert kw["force_document"] is True
    assert types.DocumentAttributeVideo not in {type(a) for a in kw["attributes"]}


@needs_ffmpeg
async def test_same_type_keeps_document_as_document(env):
    client, renamer, job = make(env, video=False)  # original was sent as a file
    await renamer.process(job)
    (_t, _f, kw), = client.sent
    assert kw["force_document"] is True
    assert types.DocumentAttributeVideo not in {type(a) for a in kw["attributes"]}


@needs_ffmpeg
async def test_forced_video_for_non_video_falls_back_to_document(env, tmp_path):
    blob = tmp_path / "data.zip"
    blob.write_bytes(b"PK" + b"0" * 5000)
    message = FakeMessage("stuff.zip", 5002, video=False, mime="application/zip")
    client = FakeClient(blob, message)
    renamer = Renamer(client, env.cfg, env.db)
    await env.db.update_settings(42, file_type="video")
    job = Job(id=2, user_id=42, chat_id=42, msg_id=7, file_name="stuff.zip", size=5002, new_name="Stuff.zip", status_msg=object())
    await renamer.process(job)
    (_t, _f, kw), = client.sent
    assert kw["force_document"] is True and kw["mime_type"] == "application/zip"
    assert kw["thumb"] is None


async def make_auto(env, thumb=None, **changes):
    """The user's auto format (made on demand), with these changes and this thumbnail."""
    fmt = await ensure_auto(env.db, 42)
    if changes:
        await env.db.update_format(42, fmt["id"], **changes)
    if thumb:
        await env.db.set_format_thumb(42, fmt["id"], thumb)
    return await env.db.get_format(42, fmt["id"])


@needs_ffmpeg
async def test_custom_thumbnail_wins_over_generated(env):
    client, renamer, job = make(env)
    job.fmt = await make_auto(env, thumb=b"\xff\xd8custom")
    await renamer.process(job)
    assert client.sent[0][2]["thumb"] == b"\xff\xd8custom"


@needs_ffmpeg
async def test_target_channel(env):
    await env.db.update_settings(42, target_channel=-100123)
    client, renamer, job = make(env)
    await renamer.process(job)
    (target, _f, kw), = client.sent
    assert target == -100123 and "reply_to" not in kw
    assert "channel" in client.edits[-1][0]


@needs_ffmpeg
async def test_audio_file_sent_as_audio_with_tags(env):
    await env.db.update_settings(42, metadata_on=True, meta={"title": "Song", "artist": "Me"})
    client, renamer, job = make(env, name="old name.mp3", src="sample.mp3", video=False, audio=True, mime="audio/mpeg")
    job.new_name = "New Song.mp3"
    await renamer.process(job)
    (_t, _f, kw), = client.sent
    audio = next(a for a in kw["attributes"] if isinstance(a, types.DocumentAttributeAudio))
    assert audio.title == "Song" and audio.performer == "Me" and audio.duration >= 1
    assert kw["force_document"] is False and kw["mime_type"] == "audio/mpeg"
    assert (await probe(client.uploads[0])).tags["title"] == "Song"


@needs_ffmpeg
async def test_unsupported_container_with_metadata_warns_but_still_renames(env, tmp_path):
    # an .avi has video streams but is outside the supported remux containers
    avi = tmp_path / "x.avi"
    import subprocess
    subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-f", "lavfi", "-i", "testsrc=duration=1:size=64x64:rate=5",
                    "-c:v", "mpeg4", str(avi)], check=True)
    message = FakeMessage("x.avi", avi.stat().st_size, video=False, mime="video/x-msvideo")
    client = FakeClient(avi, message)
    await env.db.update_settings(42, metadata_on=True, meta={"title": "T"})
    job = Job(id=3, user_id=42, chat_id=42, msg_id=7, file_name="x.avi", size=avi.stat().st_size, new_name="Y.avi", status_msg=object())
    await Renamer(client, env.cfg, env.db).process(job)
    assert client.sent and "not supported" in client.edits[-1][0]


@needs_ffmpeg
async def test_caption_rejected_is_retried_without_caption(env):
    client, renamer, job = make(env)
    client.send_errors = [errors.MediaCaptionTooLongError(request=None)]
    await renamer.process(job)
    (_t, _f, kw), = client.sent
    assert kw["caption"] is None
    assert "caption was rejected" in client.edits[-1][0] and job.state == "done"


@needs_ffmpeg
async def test_cancel_before_start(env):
    client, renamer, job = make(env)
    job.cancel.set()
    await renamer.process(job)
    assert job.state == "cancelled" and client.sent == []
    assert "Cancelled" in client.edits[-1][0]
    assert not (Path(env.cfg.temp_dir) / "job1").exists()


@pytest.fixture
async def log_channel():
    """What reaches the log channel while a test runs (the error reporter is attached to the root logger, as in main.py)."""
    reports = []

    async def send(text):
        reports.append(text)

    handler = errorlog.install(send)
    yield SimpleNamespace(reports=reports, flush=handler.aclose)
    handler.detach()


@needs_ffmpeg
async def test_failure_is_reported_and_logged(env, log_channel):
    client, renamer, job = make(env)
    client.download_error = RuntimeError("network down")
    await renamer.process(job)
    await log_channel.flush()
    assert job.state == "failed" and client.sent == []
    assert "Failed" in client.edits[-1][0] and "network down" in client.edits[-1][0]
    (report,) = log_channel.reports  # the log channel gets the whole traceback, as a Python code block
    assert report.startswith(f'<pre><code class="language-python">Error processing file:\nuser 42 · job #1\n{job.file_name} → {job.new_name} (')
    assert "Traceback (most recent call last):" in report and report.endswith("RuntimeError: network down</code></pre>")
    assert not (Path(env.cfg.temp_dir) / "job1").exists()


@needs_ffmpeg
async def test_a_finished_job_leaves_nothing_in_the_log_channel(env, log_channel):
    client, renamer, job = make(env)
    await renamer.process(job)
    await log_channel.flush()
    assert job.state == "done" and log_channel.reports == []


@needs_ffmpeg
async def test_not_enough_disk_space(env, monkeypatch):
    client, renamer, job = make(env)
    monkeypatch.setattr(shutil, "disk_usage", lambda p: SimpleNamespace(total=100, used=100, free=10))
    await renamer.process(job)
    assert job.state == "failed" and "disk space" in client.edits[-1][0]


@needs_ffmpeg
async def test_deleted_original_message_fails_cleanly(env):
    client, renamer, job = make(env)

    async def gone(chat_id, ids):
        return None

    client.get_messages = gone
    await renamer.process(job)
    assert job.state == "failed" and "deleted" in client.edits[-1][0]


def test_helpers():
    video = FakeMessage("a.mkv", 1)
    doc = FakeMessage("a.mkv", 1, video=False)
    audio = FakeMessage("a.mp3", 1, video=False, audio=True)
    assert decide_send_type("same", video) == "video"
    assert decide_send_type("same", doc) == "document"
    assert decide_send_type("same", audio) == "audio"
    assert decide_send_type("document", video) == "document"
    assert decide_send_type("audio", doc) == "audio"
    assert guess_mime("x.mkv", None) == "video/x-matroska"
    assert guess_mime("x.unknown123", "foo/bar") == "foo/bar"
    assert guess_mime("x.unknown123", None) == "application/octet-stream"
    nameless = FakeMessage(None, 1, ext=".mp4", msg_id=55)
    assert original_name_of(nameless) == "video_55.mp4"
    assert source_ext(nameless, "video_55.mp4") == ".mp4"
    assert source_ext(FakeMessage("noext", 1, ext=".mkv"), "noext") == ".mkv"


# ---------------------------------------------------------------------------
# saved /rename formats
# ---------------------------------------------------------------------------


async def make_fmt(env, name="One Piece", **changes):
    fmt = await env.db.create_format(42, name, template="One Piece - E{episode}")
    if changes:
        await env.db.update_format(42, fmt["id"], **changes)
    return await env.db.get_format(42, fmt["id"])


@needs_ffmpeg
async def test_a_format_brings_its_own_metadata_tracks_and_thumbnail(env):
    # the default metadata is off and has a different title: a manual format must not depend on either
    await env.db.update_settings(42, metadata_on=False, meta={"title": "DEFAULT"}, keep_audio=["hindi"])
    await make_auto(env, thumb=b"\xff\xd8auto")  # nor on the auto format
    fmt = await make_fmt(env, meta={"title": "One Piece", "audio": "{lang}"}, keep_audio=["english", "japanese"], keep_subtitle=["2"])
    await env.db.set_format_thumb(42, fmt["id"], b"\xff\xd8format")
    fmt = await env.db.get_format(42, fmt["id"])

    client, renamer, job = make(env)
    job.fmt = fmt
    await renamer.process(job)

    out = await probe(client.uploads[0])
    assert [s.lang for s in out.audio] == ["eng", "jpn"] and [s.lang for s in out.subtitles] == ["eng"]
    assert out.tags.get("title") == "One Piece" and [s.title for s in out.audio] == ["English", "Japanese"]
    (_t, _f, kw), = client.sent
    assert kw["thumb"] == b"\xff\xd8format"
    assert job.state == "done"


@needs_ffmpeg
async def test_a_format_without_a_thumbnail_gets_a_frame_of_the_video(env):
    await make_auto(env, thumb=b"\xff\xd8auto")  # another format's thumbnail is not borrowed
    fmt = await make_fmt(env)
    client, renamer, job = make(env)
    job.fmt = fmt
    await renamer.process(job)
    (_t, _f, kw), = client.sent
    assert kw["thumb"][:2] == b"\xff\xd8" and kw["thumb"] != b"\xff\xd8auto"  # cut out of the video


@needs_ffmpeg
async def test_a_manual_format_that_sets_no_metadata_leaves_the_file_untouched_even_if_default_metadata_is_on(env):
    await env.db.update_settings(42, metadata_on=True, meta={"title": "DEFAULT"}, keep_audio=["english"])
    fmt = await make_fmt(env)
    client, renamer, job = make(env)
    job.fmt = fmt
    await renamer.process(job)
    assert client.uploads[0].read_bytes() == (env.media_dir / "sample.mkv").read_bytes()  # no ffmpeg pass
    assert job.state == "done"


@needs_ffmpeg
async def test_an_auto_format_without_metadata_uses_the_default_metadata_while_it_is_on(env):
    await env.db.update_settings(42, metadata_on=True, meta={"title": "DEFAULT TITLE"})
    client, renamer, job = make(env)
    job.fmt = await make_auto(env)  # sets no metadata of its own
    await renamer.process(job)
    assert (await probe(client.uploads[0])).tags.get("title") == "DEFAULT TITLE"

    await env.db.update_settings(42, metadata_on=False)  # switched off: the file is left alone
    client, renamer, job = make(env)
    job.fmt = await make_auto(env)
    await renamer.process(job)
    assert client.uploads[0].read_bytes() == (env.media_dir / "sample.mkv").read_bytes()


@needs_ffmpeg
async def test_an_auto_format_with_metadata_of_its_own_ignores_the_default_metadata(env):
    await env.db.update_settings(42, metadata_on=True, meta={"title": "DEFAULT TITLE"})
    client, renamer, job = make(env)
    job.fmt = await make_auto(env, meta={"title": "OWN TITLE"})
    await renamer.process(job)
    assert (await probe(client.uploads[0])).tags.get("title") == "OWN TITLE"

    await env.db.update_settings(42, metadata_on=False)  # its own metadata applies whatever the switch says
    client, renamer, job = make(env)
    job.fmt = await make_auto(env)
    await renamer.process(job)
    assert (await probe(client.uploads[0])).tags.get("title") == "OWN TITLE"


@needs_ffmpeg
async def test_everything_else_still_comes_from_the_users_settings(env):
    await env.db.update_settings(42, file_type="document", target_channel=-100777)
    fmt = await make_fmt(env, meta={"title": "T"})
    client, renamer, job = make(env)
    job.fmt = fmt
    job.new_name = "One Piece - E5.mkv"
    await renamer.process(job)
    (target, _f, kw), = client.sent
    assert target == -100777 and kw["force_document"] is True  # the file type and the channel are still the user's
    assert kw["caption"] == "<b>One Piece - E5</b>"  # the caption is the format's: it has none, so it is the new name in bold


@needs_ffmpeg
async def test_a_format_sends_its_own_caption(env):
    from bot.utils import human_size

    await make_auto(env, caption="<i>THE AUTO FORMAT'S CAPTION {filename}</i>")  # another format's caption is not borrowed
    fmt = await make_fmt(env, caption="<b>{filename}</b> · {original} · {size}")
    client, renamer, job = make(env)
    job.fmt = fmt
    job.new_name = "One Piece - E5.mkv"
    await renamer.process(job)
    (_t, _f, kw), = client.sent
    size = (env.media_dir / "sample.mkv").stat().st_size
    assert kw["caption"] == f"<b>One Piece - E5</b> · {job.file_name} · {human_size(size)}" and kw["parse_mode"] == "html"


@needs_ffmpeg
async def test_a_format_without_a_caption_sends_the_new_name_in_bold(env):
    await make_auto(env, caption="<i>THE AUTO FORMAT'S CAPTION {filename}</i>")
    fmt = await make_fmt(env)
    client, renamer, job = make(env)
    job.fmt = fmt
    job.new_name = "One Piece - E5.mkv"
    await renamer.process(job)
    assert client.sent[0][2]["caption"] == "<b>One Piece - E5</b>"


@needs_ffmpeg
async def test_an_auto_rename_uses_the_caption_of_its_auto_format(env):
    await env.db.create_format(42, "Unused", template="X")  # a manual format changes nothing for the auto renames
    client, renamer, job = make(env)
    job.fmt = await make_auto(env, caption="<i>{filename}</i> ({quality})")
    await renamer.process(job)
    assert client.sent[0][2]["caption"] == "<i>[S01E05] Tokyo Revengers [1080p]</i> (1080p)"


@needs_ffmpeg
async def test_an_auto_rename_without_any_format_uses_the_built_in_defaults(env):
    client, renamer, job = make(env)
    assert job.fmt is None
    await renamer.process(job)
    (_t, _f, kw), = client.sent
    assert kw["caption"] == "<b>[S01E05] Tokyo Revengers [1080p]</b>"  # the new name in bold
    assert kw["thumb"][:2] == b"\xff\xd8"  # a frame of the video
    assert client.uploads[0].read_bytes() == (env.media_dir / "sample.mkv").read_bytes()
    assert await env.db.list_formats(42) == []  # nothing was made for it


@needs_ffmpeg
async def test_a_format_deleted_while_queued_still_finishes_with_its_snapshot(env):
    fmt = await make_fmt(env, meta={"title": "One Piece"})
    await env.db.set_format_thumb(42, fmt["id"], b"\xff\xd8gone")
    await env.db.delete_format(42, fmt["id"])
    client, renamer, job = make(env)
    job.fmt = fmt  # the snapshot taken when the file was queued
    await renamer.process(job)
    (_t, _f, kw), = client.sent
    assert kw["thumb"][:2] == b"\xff\xd8" and kw["thumb"] != b"\xff\xd8gone" and job.state == "done"  # its thumbnail went with it
    out = await probe(client.uploads[0])
    assert out.tags.get("title") == "One Piece"  # the snapshot's metadata is still applied


@needs_ffmpeg
async def test_the_done_message_says_how_fast_each_way_went(env, monkeypatch):
    from bot import pipeline
    from bot.utils import human_size

    ticks = iter(range(0, 1000, 2))  # every look at the clock is two seconds later than the one before
    monkeypatch.setattr(pipeline, "time", SimpleNamespace(monotonic=lambda: next(ticks)))
    client, renamer, job = make(env)
    await renamer.process(job)
    size = (env.media_dir / "sample.mkv").stat().st_size
    done = client.edits[-1][0]
    assert f"{human_size(size)} · ⬇ {human_size(size / 2)}/s · ⬆ {human_size(size / 2)}/s" in done


@needs_ffmpeg
async def test_a_transfer_that_took_under_a_second_shows_no_speed(env):
    client, renamer, job = make(env)
    await renamer.process(job)
    assert "Done" in client.edits[-1][0] and "/s" not in client.edits[-1][0]