File size: 19,993 Bytes
d043a72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import asyncio
import random
import string
import time

from aiogram import types, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import (
    Message, InlineKeyboardMarkup, InlineKeyboardButton, BufferedInputFile
)

from config import ADMINS, logger
from database import (
    db_pool, ensure_user, update_user_status, get_top_buyers,
    get_manga_queue, get_manga_queue_count, save_manga_queue,
    get_weekly_stats, build_weekly_report_text,
    get_cached_file_id, save_cached_file_id
)
from scraper import (
    collect_manga_links, get_manga_chapters_api,
    fetch_chapter_images_api, download_images_as_zip_async,
    download_images_as_pdf_async, get_manga_slug
)
from handlers import AdminState, safe_edit, safe_answer, back_kb

# =========================
# CACHING CONTROL
# =========================
caching_control = {"is_running": False, "stop_requested": False}

# =========================
# ADMIN KEYBOARD
# =========================
def get_admin_kb():
    return InlineKeyboardMarkup(inline_keyboard=[
        [InlineKeyboardButton(text="๐Ÿ“Š Stats & Leaderboard",         callback_data="admin_stats")],
        [
            InlineKeyboardButton(text="๐Ÿ’ฐ Add Coins",                callback_data="admin_add_coins"),
            InlineKeyboardButton(text="๐ŸŒŸ Set VIP",                  callback_data="admin_set_vip"),
        ],
        [
            InlineKeyboardButton(text="๐Ÿ“ก Broadcast",                callback_data="admin_broadcast"),
            InlineKeyboardButton(text="โœ‰๏ธ Send PM",                  callback_data="admin_pm"),
        ],
        [InlineKeyboardButton(text="๐ŸŽŸ Voucher Guide",               callback_data="admin_voucher_guide")],
        [InlineKeyboardButton(text="๐Ÿ”— Fetch Links (71 Pages)",      callback_data="admin_fetch_links")],
        [
            InlineKeyboardButton(text="๐Ÿš€ Cache ZIP (Saved Links)", callback_data="admin_start_cache_saved_zip"),
            InlineKeyboardButton(text="๐Ÿ“„ Cache PDF (Saved Links)", callback_data="admin_start_cache_saved_pdf"),
        ],
        [InlineKeyboardButton(text="ยซ Back",                         callback_data="back_to_start")],
    ])

# =========================
# BACKGROUND CACHE TASK
# =========================
async def background_cache_task(bot, admin_id: int, manga_url: str, file_format: str = "zip"):
    from database import get_manga_slug as _slug
    slug     = _slug(manga_url)
    chapters = await get_manga_chapters_api(manga_url)
    if not chapters:
        return
    for chap in reversed(chapters):
        if caching_control["stop_requested"]:
            break
        ch_id = chap["id"]
        if get_cached_file_id(slug, ch_id, file_format):
            continue
        images = await fetch_chapter_images_api(ch_id)
        if not images:
            continue
        try:
            if file_format == "pdf":
                file_data = await download_images_as_pdf_async(images, slug, ch_id)
                file_name = f"{slug}_chapter-{chap['slug']}.pdf"
            else:
                file_data = await download_images_as_zip_async(images, slug, ch_id)
                file_name = f"{slug}_chapter-{chap['slug']}.zip"
            sent_msg  = await bot.send_document(
                chat_id=admin_id,
                document=BufferedInputFile(file_data, filename=file_name),
                caption=f"โš™๏ธ Cached ({file_format.upper()}): {slug} โ€” {chap['title']}",
                disable_notification=True
            )
            new_file_id = sent_msg.document.file_id
            save_cached_file_id(slug, ch_id, file_format, new_file_id)
            await asyncio.sleep(20)
        except Exception as e:
            logger.error(f"Error caching chapter {ch_id}: {e}")
            await asyncio.sleep(10)

async def _fetch_and_save_links(bot, admin_id: int):
    async def progress_cb(page_num, count):
        try:
            await bot.send_message(admin_id, f"๐Ÿ“„ ุชู… ุณุญุจ ุตูุญุฉ {page_num}/71 โ€” ุฑูˆุงุจุท ุญุชู‰ ุงู„ุขู†: {count}")
        except Exception:
            pass

    all_links = await collect_manga_links(
        start_page=1, end_page=71,
        stop_flag=caching_control,
        progress_cb=progress_cb
    )
    save_manga_queue(all_links)
    try:
        await bot.send_message(
            admin_id,
            f"โœ… *ุงูƒุชู…ู„ ุณุญุจ ุงู„ุฑูˆุงุจุท!*\n\n"
            f"๐Ÿ”— ุฅุฌู…ุงู„ูŠ ุงู„ุฑูˆุงุจุท ุงู„ู…ุญููˆุธุฉ: *{len(all_links)}*\n\n"
            f"ุงุถุบุท ุนู„ู‰ ุฒุฑ ZIP ุฃูˆ PDF ู„ุจุฏุก ุงู„ุฃุฑุดูุฉ."
        )
    except Exception:
        pass

async def _cache_from_queue(bot, admin_id: int, manga_links: list, file_format: str = "zip"):
    caching_control["is_running"]     = True
    caching_control["stop_requested"] = False
    total = len(manga_links)
    try:
        await bot.send_message(admin_id, f"๐Ÿ›ฐ ุจุฏุฃุช ุงู„ุฃุฑุดูุฉ ({file_format.upper()}): {total} ู…ุงู†ุฌุง ููŠ ุงู„ุทุงุจูˆุฑ.")
        for index, manga_url in enumerate(manga_links, 1):
            if caching_control["stop_requested"]:
                await bot.send_message(admin_id, "๐Ÿ›‘ ุชู… ุฅูŠู‚ุงู ุงู„ุฃุฑุดูุฉ ุจู†ุงุกู‹ ุนู„ู‰ ุทู„ุจูƒ.")
                break
            try:
                if index % 10 == 0 or index == 1:
                    await bot.send_message(admin_id, f"๐Ÿ“Š ุงู„ุชู‚ุฏู…: {index}/{total}")
                await background_cache_task(bot, admin_id, manga_url, file_format=file_format)
                await asyncio.sleep(5)
            except Exception as e:
                logger.error(f"Error caching {manga_url}: {e}")
    finally:
        caching_control["is_running"]     = False
        caching_control["stop_requested"] = False
        try:
            await bot.send_message(admin_id, f"โœ… ุงูƒุชู…ู„ุช ุนู…ู„ูŠุฉ ุงู„ุฃุฑุดูุฉ ({file_format.upper()}) ู…ู† ุงู„ุทุงุจูˆุฑ ุงู„ู…ุญููˆุธ.")
        except Exception:
            pass

# =========================
# REGISTER ADMIN HANDLERS
# =========================
def register_admin_handlers(dp, bot):

    @dp.callback_query(F.data == "admin_btn")
    async def admin_panel(cb: types.CallbackQuery):
        if cb.from_user.id not in ADMINS:
            return await cb.answer("Unauthorized!", show_alert=True)
        await cb.answer()
        await safe_edit(cb.message, "โš™๏ธ *Admin Panel*\n\nChoose an action:", reply_markup=get_admin_kb())

    @dp.callback_query(F.data == "admin_stats")
    async def admin_stats(cb: types.CallbackQuery):
        if cb.from_user.id not in ADMINS: return await cb.answer("Unauthorized!", show_alert=True)
        await cb.answer()
        conn = db_pool.getconn()
        try:
            with conn.cursor() as c:
                c.execute("SELECT COUNT(*) FROM users")
                total_users = c.fetchone()[0]
                c.execute("SELECT COUNT(*) FROM opened_chapters")
                total_opened = c.fetchone()[0]
                c.execute("SELECT COUNT(*) FROM users WHERE vip_until > %s", (int(time.time()),))
                total_vip = c.fetchone()[0]
                c.execute("SELECT COUNT(*) FROM manga_queue")
                queued_links = c.fetchone()[0]
                c.execute("SELECT COUNT(*) FROM chapter_cache")
                cached_chapters = c.fetchone()[0]
        finally:
            db_pool.putconn(conn)
        top    = get_top_buyers(5)
        medals = ["๐Ÿฅ‡","๐Ÿฅˆ","๐Ÿฅ‰","4๏ธโƒฃ","5๏ธโƒฃ"]
        board  = "\n".join(
            f"{medals[i]} {('@'+uname) if uname else uid} โ€” *{cnt}* chapters"
            for i, (uid, uname, cnt) in enumerate(top)
        ) or "No data yet"
        await safe_edit(
            cb.message,
            f"๐Ÿ“Š *Bot Stats*\n\n"
            f"๐Ÿ‘ฅ Total Users: *{total_users}*\n"
            f"๐ŸŒŸ VIP Users: *{total_vip}*\n"
            f"๐Ÿ“– Total Chapters Opened: *{total_opened}*\n"
            f"๐Ÿ’พ Cached Chapters: *{cached_chapters}*\n"
            f"๐Ÿ”— Queued Links: *{queued_links}*\n\n"
            f"๐Ÿ† *Top Buyers:*\n{board}",
            reply_markup=back_kb("admin_btn")
        )

    @dp.callback_query(F.data == "admin_voucher_guide")
    async def admin_voucher_guide(cb: types.CallbackQuery):
        if cb.from_user.id not in ADMINS: return await cb.answer("Unauthorized!", show_alert=True)
        await safe_edit(
            cb.message,
            "๐ŸŽŸ *Voucher Creation Guide*\n\n"
            "โ€ข `/gen_code coins 1000` โ€” Creates a 1000 coin code\n"
            "โ€ข `/gen_code vip 7` โ€” Creates a 7-day VIP code\n"
            "โ€ข `/gen_code vip inf` โ€” Creates a Lifetime VIP code\n",
            reply_markup=back_kb("admin_btn")
        )

    @dp.callback_query(F.data == "admin_add_coins")
    async def admin_add_coins_btn(cb: types.CallbackQuery, state: FSMContext):
        if cb.from_user.id not in ADMINS: return await cb.answer("Unauthorized!", show_alert=True)
        await cb.answer()
        await state.set_state(AdminState.waiting_add_coins)
        await safe_edit(
            cb.message,
            "๐Ÿ’ฐ *Add Coins*\n\nSend: `USER_ID AMOUNT [Optional Message]`\nExample: `12345 500 Enjoy!`",
            reply_markup=back_kb("admin_btn")
        )

    @dp.message(AdminState.waiting_add_coins)
    async def admin_add_coins_input(m: Message, state: FSMContext):
        if m.from_user.id not in ADMINS: return
        try:
            parts = m.text.split(maxsplit=2)
            uid, amount = int(parts[0]), int(parts[1])
            msg_to_user = parts[2] if len(parts) > 2 else "An admin has sent you some coins! ๐ŸŽ‰"
            user = ensure_user(uid)
            update_user_status(uid, user[2] + amount)
            await state.clear()
            await safe_answer(
                m,
                f"โœ… Added *{amount}* coins to `{uid}`\nNew balance: *{user[2] + amount}*",
                reply_markup=back_kb("admin_btn")
            )
            try:
                await bot.send_message(uid, f"๐Ÿ’ฐ *{amount} Coins Added!*\n\nMessage: _{msg_to_user}_")
            except Exception:
                pass
        except Exception:
            await m.answer("โŒ Format: `USER_ID AMOUNT [MESSAGE]`")

    @dp.callback_query(F.data == "admin_set_vip")
    async def admin_set_vip_btn(cb: types.CallbackQuery, state: FSMContext):
        if cb.from_user.id not in ADMINS: return await cb.answer("Unauthorized!", show_alert=True)
        await cb.answer()
        await state.set_state(AdminState.waiting_vip)
        await safe_edit(
            cb.message,
            "๐ŸŒŸ *Set VIP*\n\nSend: `USER_ID DAYS [Message]` or `USER_ID inf [Message]`",
            reply_markup=back_kb("admin_btn")
        )

    @dp.message(AdminState.waiting_vip)
    async def admin_vip_input(m: Message, state: FSMContext):
        if m.from_user.id not in ADMINS: return
        try:
            parts = m.text.split(maxsplit=2)
            uid, d = int(parts[0]), parts[1].lower()
            msg_to_user = parts[2] if len(parts) > 2 else "An admin has upgraded your account to VIP! ๐ŸŒŸ"
            expire = 9999999999 if d == "inf" else int(time.time()) + int(d) * 86400
            user   = ensure_user(uid)
            update_user_status(uid, user[2], expire)
            await state.clear()
            vip_text = "Lifetime" if d == "inf" else f"{d} days"
            await safe_answer(m, f"โœ… VIP set for `{uid}` โ€” *{vip_text}*", reply_markup=back_kb("admin_btn"))
            try:
                await bot.send_message(uid, f"๐ŸŒŸ *VIP Status Activated!* ({vip_text})\n\nMessage: _{msg_to_user}_")
            except Exception:
                pass
        except Exception:
            await m.answer("โŒ Format: `USER_ID DAYS/inf [MESSAGE]`")

    @dp.callback_query(F.data == "admin_pm")
    async def admin_pm_btn(cb: types.CallbackQuery, state: FSMContext):
        if cb.from_user.id not in ADMINS: return await cb.answer("Unauthorized!", show_alert=True)
        await cb.answer()
        await state.set_state(AdminState.waiting_pm)
        await safe_edit(cb.message, "โœ‰๏ธ *Send PM*\n\nSend: `USER_ID Your Message Here`",
                        reply_markup=back_kb("admin_btn"))

    @dp.message(AdminState.waiting_pm)
    async def admin_pm_input(m: Message, state: FSMContext):
        if m.from_user.id not in ADMINS: return
        try:
            parts = m.text.split(maxsplit=1)
            uid   = int(parts[0])
            msg   = parts[1]
            await state.clear()
            try:
                await bot.send_message(uid, f"๐Ÿ“ฉ *Message from Admin:*\n\n{msg}")
                await safe_answer(m, f"โœ… PM sent to `{uid}`", reply_markup=back_kb("admin_btn"))
            except Exception as e:
                await safe_answer(m, f"โŒ Failed: {e}", reply_markup=back_kb("admin_btn"))
        except Exception:
            await m.answer("โŒ Format: `USER_ID Message`")

    @dp.callback_query(F.data == "admin_broadcast")
    async def admin_broadcast_btn(cb: types.CallbackQuery, state: FSMContext):
        if cb.from_user.id not in ADMINS: return await cb.answer("Unauthorized!", show_alert=True)
        await cb.answer()
        await state.set_state(AdminState.waiting_broadcast)
        await safe_edit(cb.message, "๐Ÿ“ก *Broadcast*\n\nSend your message now:", reply_markup=back_kb("admin_btn"))

    @dp.message(AdminState.waiting_broadcast)
    async def admin_broadcast_input(m: Message, state: FSMContext):
        if m.from_user.id not in ADMINS: return
        text = m.text.strip()
        await state.clear()
        conn = db_pool.getconn()
        try:
            with conn.cursor() as c:
                c.execute("SELECT user_id FROM users")
                users = c.fetchall()
        finally:
            db_pool.putconn(conn)
        sent    = 0
        msg_obj = await m.answer(f"๐Ÿ“ก Sending to {len(users)} users...")
        for i, (uid,) in enumerate(users):
            try:
                await bot.send_message(uid, text)
                sent += 1
            except Exception:
                pass
            if i % 20 == 0:
                try: await msg_obj.edit_text(f"๐Ÿ“ก {i}/{len(users)}")
                except Exception: pass
        await msg_obj.edit_text(f"โœ… Done: *{sent}/{len(users)}*", reply_markup=back_kb("admin_btn"))

    # โ”€โ”€ Voucher generation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    def _gen_code(length=10):
        return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length))

    @dp.message(Command("gen_code"))
    async def generate_voucher(m: Message):
        if m.from_user.id not in ADMINS: return
        parts = m.text.split()
        if len(parts) < 3:
            return await m.answer("Usage:\n`/gen_code coins 100`\n`/gen_code vip 7`\n`/gen_code vip inf`")
        code_type = parts[1].lower()
        value     = parts[2].lower()
        code      = _gen_code()
        conn = db_pool.getconn()
        try:
            with conn.cursor() as c:
                if code_type == "coins":
                    coins = int(value)
                    c.execute("INSERT INTO vouchers (code, coins) VALUES (%s,%s)", (code, coins))
                    msg = f"๐ŸŽŸ Code Created!\n\nCode: `{code}`\n๐Ÿ’ฐ Coins: *{coins}*"
                elif code_type == "vip":
                    vip_days = -1 if value == "inf" else int(value)
                    c.execute("INSERT INTO vouchers (code, vip_days) VALUES (%s,%s)", (code, vip_days))
                    msg = f"๐ŸŽŸ Code Created!\n\nCode: `{code}`\n๐ŸŒŸ VIP: *{'Lifetime' if value=='inf' else value + ' days'}*"
                else:
                    return await m.answer("โŒ Type must be `coins` or `vip`")
                conn.commit()
        finally:
            db_pool.putconn(conn)
        await m.answer(msg)

    @dp.message(Command("pm"))
    async def pm_cmd(m: Message):
        if m.from_user.id not in ADMINS: return
        try:
            parts = m.text.split(maxsplit=2)
            uid   = int(parts[1])
            msg   = parts[2]
            await bot.send_message(uid, f"๐Ÿ“ฉ *Message from Admin:*\n\n{msg}")
            await m.answer(f"โœ… Sent to `{uid}`")
        except Exception:
            await m.answer("Usage: `/pm USER_ID Message`")

    # โ”€โ”€ Fetch Links & Cache Buttons โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    @dp.callback_query(F.data == "admin_fetch_links")
    async def admin_fetch_links_btn(cb: types.CallbackQuery):
        if cb.from_user.id not in ADMINS:
            return await cb.answer("Unauthorized!", show_alert=True)
        if caching_control["is_running"]:
            return await cb.answer("โš ๏ธ ุนู…ู„ูŠุฉ ุฃุฑุดูุฉ ุชุนู…ู„ ุงู„ุขู†! ุฃูˆู‚ูู‡ุง ุฃูˆู„ุงู‹.", show_alert=True)
        await cb.answer("๐Ÿ”— ุฌุงุฑูŠ ุณุญุจ ุงู„ุฑูˆุงุจุท...", show_alert=True)
        await safe_edit(
            cb.message,
            "๐Ÿ”— *ุฌุงุฑูŠ ุณุญุจ ุฑูˆุงุจุท ุงู„ู€ 71 ุตูุญุฉ...*\n\n"
            "ุณูŠุชู… ุฅุฑุณุงู„ ุชุญุฏูŠุซ ูƒู„ 10 ุตูุญุงุช.\n"
            "ุจุนุฏ ุงู„ุงู†ุชู‡ุงุก ุงุถุบุท ุฒุฑ ุงู„ูƒุงุดูŠู†ุฌ.",
            reply_markup=back_kb("admin_btn")
        )
        asyncio.create_task(_fetch_and_save_links(bot, cb.from_user.id))

    @dp.callback_query(F.data.in_({"admin_start_cache_saved_zip", "admin_start_cache_saved_pdf"}))
    async def admin_start_cache_saved_btn(cb: types.CallbackQuery):
        if cb.from_user.id not in ADMINS:
            return await cb.answer("Unauthorized!", show_alert=True)
        if caching_control["is_running"]:
            return await cb.answer("โš ๏ธ ุงู„ุฃุฑุดูุฉ ุชุนู…ู„ ุจุงู„ูุนู„! ุงุณุชุฎุฏู… /stop_cache ุฃูˆู„ุงู‹.", show_alert=True)
        queue = get_manga_queue()
        if not queue:
            return await cb.answer("โŒ ู„ุง ุชูˆุฌุฏ ุฑูˆุงุจุท ู…ุญููˆุธุฉ! ุงุณุญุจ ุงู„ุฑูˆุงุจุท ุฃูˆู„ุงู‹.", show_alert=True)
        file_format = "pdf" if cb.data.endswith("_pdf") else "zip"
        await cb.answer(f"๐Ÿš€ ุจุฏุฃ ุงู„ูƒุงุดูŠู†ุฌ ({file_format.upper()}) ู…ู† ุงู„ุฑูˆุงุจุท ุงู„ู…ุญููˆุธุฉ...", show_alert=True)
        await safe_edit(
            cb.message,
            f"๐Ÿš€ *ุจุฏุฃ ุงู„ูƒุงุดูŠู†ุฌ!*\n\n"
            f"๐Ÿ”— ุนุฏุฏ ุงู„ุฑูˆุงุจุท: *{len(queue)}*\n"
            f"๐Ÿ—‚ ุงู„ู†ูˆุน: *{file_format.upper()}*\n"
            f"ุงุณุชุฎุฏู… /stop_cache ู„ู„ุฅูŠู‚ุงู.",
            reply_markup=back_kb("admin_btn")
        )
        asyncio.create_task(_cache_from_queue(bot, cb.from_user.id, queue, file_format=file_format))

    @dp.message(Command("cache_site"))
    async def cache_site_cmd(m: Message):
        if m.from_user.id not in ADMINS: return
        if caching_control["is_running"]:
            return await m.answer("โš ๏ธ ุงู„ุฃุฑุดูุฉ ุชุนู…ู„! ุงุณุชุฎุฏู… /stop_cache ุฃูˆู„ุงู‹.")
        parts   = m.text.split()
        start_p = int(parts[1]) if len(parts) >= 2 and parts[1].isdigit() else 1
        end_p   = int(parts[2]) if len(parts) >= 3 and parts[2].isdigit() else 71
        queue   = get_manga_queue()
        if not queue:
            await m.answer(f"๐Ÿ”— ู„ุง ุชูˆุฌุฏ ุฑูˆุงุจุท ู…ุญููˆุธุฉ. ุณูŠุชู… ุณุญุจู‡ุง ุฃูˆู„ุงู‹ ู…ู† ุตูุญุฉ {start_p} ุฅู„ู‰ {end_p}.")
            asyncio.create_task(_fetch_and_save_links(bot, m.from_user.id))
        else:
            asyncio.create_task(_cache_from_queue(bot, m.from_user.id, queue, file_format="zip"))
            await m.answer(f"๐Ÿ›ฐ ุจุฏุฃุช ุงู„ุฃุฑุดูุฉ ZIP ู…ู† ุงู„ุฑูˆุงุจุท ุงู„ู…ุญููˆุธุฉ ({len(queue)} ู…ุงู†ุฌุง).")

    @dp.message(Command("stop_cache"))
    async def stop_cache_cmd(m: Message):
        if m.from_user.id not in ADMINS: return
        if not caching_control["is_running"]:
            return await m.answer("โŒ ู„ุง ุชูˆุฌุฏ ุนู…ู„ูŠุฉ ุฃุฑุดูุฉ ุชุนู…ู„ ุญุงู„ูŠุงู‹.")
        caching_control["stop_requested"] = True
        await m.answer("โณ ุฌุงุฑูŠ ุทู„ุจ ุงู„ุฅูŠู‚ุงู... ุณูŠุชูˆู‚ู ุจุนุฏ ุงู„ู…ู‡ู…ุฉ ุงู„ุญุงู„ูŠุฉ.")