{"id":28,"date":"2026-01-27T11:55:54","date_gmt":"2026-01-27T11:55:54","guid":{"rendered":"https:\/\/wordpress-fz3fv.wasmer.app\/?p=28"},"modified":"2026-04-15T07:03:37","modified_gmt":"2026-04-15T07:03:37","slug":"tg-forward-videos-plus","status":"publish","type":"post","link":"https:\/\/wp.bao.ksapp.me\/?p=28","title":{"rendered":"TG-forward-videos-plus3"},"content":{"rendered":"\n<pre class=\"wp-block-code\"><code>import asyncio\nimport os\nimport random\nimport logging\nimport hashlib\nfrom typing import List, Dict\nimport aiosqlite\nfrom telethon import TelegramClient\nfrom telethon.tl.types import MessageMediaDocument, DocumentAttributeVideo, Message\nfrom telethon.errors import (\n    FloodWaitError, \n    SecurityError, \n    FileReferenceExpiredError,\n    rpcerrorlist\n)\nfrom dotenv import load_dotenv\n\n# \u52a0\u8f7d\u73af\u5883\u53d8\u91cf\nload_dotenv()\n\n# ==================== \ud83d\udcc1 \u6587\u4ef6\u5939\u521d\u59cb\u5316 ====================\nDB_FOLDER = \"database\"\nSESSION_FOLDER = \"session\"\n\nfor folder in &#91;DB_FOLDER, SESSION_FOLDER]:\n    if not os.path.exists(folder):\n        os.makedirs(folder)\n\n# ==================== \ud83d\udee0\ufe0f \u914d\u7f6e\u8bfb\u53d6\u5e2e\u52a9\u51fd\u6570 ====================\ndef get_env_float(key, default=0.0):\n    val = os.getenv(key, \"\")\n    if not val: return default\n    try:\n        return float(val)\n    except ValueError:\n        print(f\"\u26a0\ufe0f \u914d\u7f6e\u9519\u8bef: {key} \u5fc5\u987b\u662f\u6570\u5b57\uff0c\u5f53\u524d\u4e3a '{val}'\uff0c\u5df2\u91cd\u7f6e\u4e3a {default}\")\n        return default\n\ndef get_env_int(key, default=0):\n    val = os.getenv(key, \"\")\n    if not val: return default\n    try:\n        return int(val)\n    except ValueError:\n        return default\n\n# ==================== \u2699\ufe0f \u57fa\u7840\u914d\u7f6e ====================\nAPI_ID = get_env_int(\"API_ID\")\nAPI_HASH = os.getenv(\"API_HASH\")\nPHONE_NUMBER = os.getenv(\"PHONE_NUMBER\")\n\nMIN_SIZE_MB = get_env_float(\"MIN_SIZE_MB\", 0)\nMAX_SIZE_MB = get_env_float(\"MAX_SIZE_MB\", 0)\nMIN_DURATION = get_env_int(\"MIN_DURATION\", 0)\nMAX_DURATION = get_env_int(\"MAX_DURATION\", 0)\n\nSTOP_AFTER_DUPLICATES = 100 \nMAX_SCAN_COUNT = 22000 \n\nMIN_INTERVAL = 2\nMAX_INTERVAL = 5\nALBUM_WAIT_TIME = 3.0\n\n# ==================== \ud83d\udccb \u4efb\u52a1\u6e05\u5355 ====================\nTASK_CONFIG = &#91;\n    # ADULT INDUSTRY \u5230 town1\n    {\n        \"sources\": &#91;-1001245945922],\n        \"target\": -xxx,\n        \"limit\": 300\n    },\n   \n]\n\n# ==================== \u65e5\u5fd7\u521d\u59cb\u5316 ====================\nlogging.basicConfig(\n    level=logging.INFO, \n    format=\"%(asctime)s - %(message)s\",\n    datefmt=\"%H:%M:%S\"\n)\nlogger = logging.getLogger(\"AutoBot\")\n\n# --- \u4fee\u6539\uff1aSession \u8def\u5f84\u79fb\u52a8\u5230 session \u6587\u4ef6\u5939 ---\nsession_path = os.path.join(SESSION_FOLDER, \"user_session\")\nclient = TelegramClient(session_path, API_ID, API_HASH)\n\n# \u5168\u5c40\u72b6\u6001\u53d8\u91cf\nforward_queue = None\npending_albums = {}\nalbum_timers = {}\ndb = None\n\n# ==================== \ud83d\uddc4\ufe0f \u6570\u636e\u5e93\u7c7b ====================\nclass AsyncDB:\n    def __init__(self, path):\n        self.path = path\n        self.conn = None\n        self.lock = asyncio.Lock()\n\n    async def connect(self):\n        self.conn = await aiosqlite.connect(self.path)\n        await self.conn.execute(\"CREATE TABLE IF NOT EXISTS videos (video_key TEXT PRIMARY KEY)\")\n        await self.conn.commit()\n\n    async def close(self):\n        if self.conn: await self.conn.close()\n\n    async def seen(self, key):\n        async with self.lock:\n            async with self.conn.execute(\"SELECT 1 FROM videos WHERE video_key=?\", (key,)) as cursor:\n                return await cursor.fetchone() is not None\n\n    async def mark(self, key):\n        async with self.lock:\n            await self.conn.execute(\"INSERT OR IGNORE INTO videos (video_key) VALUES (?)\", (key,))\n            await self.conn.commit()\n\n# ==================== \ud83d\udee0\ufe0f \u6838\u5fc3\u5de5\u5177\u51fd\u6570 ====================\n\ndef get_unique_key(msg: Message) -> str:\n    return f\"{msg.chat_id}_{msg.media.document.id}\"\n\ndef generate_safe_filename(sources: List&#91;int], target: int) -> str:\n    \"\"\"\u4fee\u6539\uff1a\u786e\u4fdd\u6587\u4ef6\u540d\u5305\u542b\u6587\u4ef6\u5939\u8def\u5f84\"\"\"\n    sources_str = \"_\".join(&#91;str(abs(s)) for s in sources])\n    target_str = str(abs(target))\n    \n    if len(sources_str) > 50:\n        hash_object = hashlib.md5(sources_str.encode())\n        short_hash = hash_object.hexdigest()&#91;:8]\n        preview = \"_\".join(&#91;str(abs(s)) for s in sources&#91;:2]])\n        fname = f\"{preview}_etc_{short_hash}_to_{target_str}.db\"\n    else:\n        fname = f\"{sources_str}to{target_str}.db\"\n    \n    # \u62fc\u63a5\u6570\u636e\u5e93\u6587\u4ef6\u5939\u8def\u5f84\n    return os.path.join(DB_FOLDER, fname)\n\ndef is_video(msg: Message) -> bool:\n    if not msg.media or not isinstance(msg.media, MessageMediaDocument): \n        return False\n    doc = msg.media.document\n    if not doc.mime_type.startswith(\"video\"): \n        return False\n    video_attr = next((a for a in doc.attributes if isinstance(a, DocumentAttributeVideo)), None)\n    if getattr(video_attr, \"round_message\", False): \n        return False\n    size_mb = doc.size \/ (1024.0 * 1024.0)\n    if MIN_SIZE_MB > 0 and size_mb &lt; MIN_SIZE_MB: return False\n    if MAX_SIZE_MB > 0 and size_mb > MAX_SIZE_MB: return False\n    if video_attr:\n        duration = video_attr.duration\n        if MIN_DURATION > 0 and duration &lt; MIN_DURATION: return False\n        if MAX_DURATION > 0 and duration > MAX_DURATION: return False\n    return True\n\n# ==================== \ud83d\udd04 \u5f02\u6b65\u903b\u8f91 ====================\n\nasync def process_album_later(grouped_id):\n    try:\n        await asyncio.sleep(ALBUM_WAIT_TIME)\n        if grouped_id in pending_albums:\n            messages = pending_albums.pop(grouped_id)\n            album_timers.pop(grouped_id, None)\n            if messages:\n                messages.sort(key=lambda x: x.id)\n                await forward_queue.put(messages)\n    except asyncio.CancelledError:\n        pass\n\nasync def queue_message(message: Message):\n    if message.grouped_id:\n        gid = message.grouped_id\n        if gid not in pending_albums: pending_albums&#91;gid] = &#91;]\n        pending_albums&#91;gid].append(message)\n        if gid in album_timers: album_timers&#91;gid].cancel()\n        album_timers&#91;gid] = asyncio.create_task(process_album_later(gid))\n    else:\n        await forward_queue.put(&#91;message])\n\nasync def worker(target_channel_id):\n    processed_count = 0\n    while True:\n        try:\n            batch = await forward_queue.get()\n            if batch is None: \n                forward_queue.task_done()\n                break\n            files = &#91;m.media for m in batch]\n            caption = batch&#91;0].text or \"\"\n            try:\n                await client.send_file(target_channel_id, files, caption=caption)\n                processed_count += len(batch)\n                logger.info(f\"\u2705 \u53d1\u9001\u6210\u529f | \u672c\u6b21: {len(batch)} | \u603b\u8ba1: {processed_count}\")\n                for m in batch: \n                    await db.mark(get_unique_key(m))\n                await asyncio.sleep(random.uniform(MIN_INTERVAL, MAX_INTERVAL))\n            except FloodWaitError as e:\n                logger.warning(f\"\u23f3 \u89e6\u53d1\u6d41\u63a7: \u6682\u505c {e.seconds} \u79d2\")\n                await asyncio.sleep(e.seconds + 2)\n            except FileReferenceExpiredError:\n                logger.error(\"\u274c \u6587\u4ef6\u5f15\u7528\u8fc7\u671f\uff0c\u8df3\u8fc7\")\n            except Exception as e:\n                logger.error(f\"\u274c \u53d1\u9001\u5f02\u5e38: {e}\")\n            forward_queue.task_done()\n        except Exception as e:\n            logger.error(f\"Worker Error: {e}\")\n\nasync def scanner(source_channels, forward_limit):\n    all_collected_videos = &#91;]\n    for ch in source_channels:\n        channel_collected = 0 \n        logger.info(f\"\ud83d\udd0d \u6b63\u5728\u626b\u63cf: {ch} (\u76ee\u6807: {forward_limit})\")\n        consecutive_duplicates = 0\n        scanned_count = 0\n        async for msg in client.iter_messages(ch, limit=MAX_SCAN_COUNT):\n            scanned_count += 1\n            if scanned_count % 200 == 0:\n                print(f\"\\r   ...\u626b\u63cf\u6df1\u5ea6: {scanned_count} \u6761\", end=\"\")\n            if not is_video(msg): continue\n            if await db.seen(get_unique_key(msg)):\n                consecutive_duplicates += 1\n                if consecutive_duplicates >= STOP_AFTER_DUPLICATES:\n                    print(f\"\\n\ud83d\uded1 &#91;\u9891\u9053 {ch}] \u8fde\u7eed {STOP_AFTER_DUPLICATES} \u6761\u91cd\u590d\uff0c\u505c\u6b62\u626b\u63cf\u3002\")\n                    break\n            else:\n                consecutive_duplicates = 0\n                all_collected_videos.append(msg)\n                channel_collected += 1\n                print(f\"\\r\ud83d\udce6 &#91;\u9891\u9053 {ch}] \u547d\u4e2d: {channel_collected}\/{forward_limit}\", end=\"\")\n            if channel_collected >= forward_limit:\n                print(f\"\\n\u2705 &#91;\u9891\u9053 {ch}] \u989d\u5ea6\u5df2\u6ee1\")\n                break\n        print(\"\")\n\n    if all_collected_videos:\n        logger.info(f\"\ud83d\udcca \u627e\u5230 {len(all_collected_videos)} \u4e2a\u65b0\u89c6\u9891\u3002\u6392\u5e8f\u5165\u961f...\")\n        all_collected_videos.sort(key=lambda x: x.date)\n        for msg in all_collected_videos:\n            await queue_message(msg)\n        if pending_albums:\n            logger.info(\"\u23f3 \u7b49\u5f85\u76f8\u518c\u5206\u7ec4...\")\n            while pending_albums or album_timers:\n                finished = &#91;k for k, t in album_timers.items() if t.done()]\n                for k in finished: del album_timers&#91;k]\n                if not album_timers and not pending_albums: break\n                await asyncio.sleep(0.5)\n    else:\n        logger.info(\"\u26a0\ufe0f \u6ca1\u6709\u53d1\u73b0\u4efb\u4f55\u65b0\u89c6\u9891\u3002\")\n    await forward_queue.put(None)\n\nasync def run_single_task(task):\n    global db, forward_queue, pending_albums, album_timers\n    \n    sources = task&#91;'sources']\n    target = task&#91;'target']\n    limit = task.get('limit', 200)\n\n    # \u8fd9\u91cc generate_safe_filename \u5df2\u7ecf\u5305\u542b database \u8def\u5f84\n    full_db_path = generate_safe_filename(sources, target)\n    logger.info(f\"\\n\ud83d\ude80 >>> \u542f\u52a8\u4efb\u52a1: {os.path.basename(full_db_path)} &lt;&lt;&lt;\")\n    \n    # \u5f7b\u5e95\u91cd\u7f6e\u72b6\u6001\n    forward_queue = asyncio.Queue()\n    pending_albums = {}\n    album_timers = {}\n    \n    db = AsyncDB(full_db_path)\n    await db.connect()\n    \n    worker_task = asyncio.create_task(worker(target))\n    \n    try:\n        await scanner(sources, limit)\n        await forward_queue.join()\n    except Exception as e:\n        logger.error(f\"\u274c \u4efb\u52a1\u8fd0\u884c\u5f02\u5e38: {e}\")\n    finally:\n        if not worker_task.done():\n            worker_task.cancel()\n        await db.close()\n        logger.info(f\"\ud83c\udfc1 >>> \u4efb\u52a1\u7ed3\u675f: {os.path.basename(full_db_path)} &lt;&lt;&lt;\\n\")\n\n# ==================== \ud83d\ude80 \u4e3b\u7a0b\u5e8f\u5165\u53e3 ====================\n\nasync def main():\n    print(\"=\"*50)\n    print(\"\ud83c\udfa5  Auto-Forwarder Pro (Task Sequencer)\")\n    print(f\"\ud83d\udcc9  \u6700\u5c0f\u89c6\u9891\u9650\u5236: {MIN_SIZE_MB} MB\")\n    print(f\"\ud83d\udcc8  \u6700\u5927\u89c6\u9891\u9650\u5236: {MAX_SIZE_MB if MAX_SIZE_MB > 0 else '\u4e0d\u9650'} MB\")\n    print(\"=\"*50)\n    \n    if not TASK_CONFIG:\n        logger.error(\"\u274c TASK_CONFIG \u4e3a\u7a7a\uff0c\u8bf7\u5728\u4ee3\u7801\u4e2d\u914d\u7f6e\u4efb\u52a1\u5217\u8868\uff01\")\n        return\n\n    await client.start(PHONE_NUMBER)\n    \n    try:\n        total_tasks = len(TASK_CONFIG)\n        for index, task in enumerate(TASK_CONFIG):\n            await run_single_task(task)\n            \n            if index &lt; total_tasks - 1:\n                wait_sec = 10\n                logger.info(f\"\u23f3 \u4efb\u52a1 &#91;{index + 1}\/{total_tasks}] \u5df2\u5b8c\u6210\u3002\")\n                logger.info(f\"\u23f3 \u51b7\u5374\u4e2d... {wait_sec} \u79d2\u540e\u6267\u884c\u4e0b\u4e00\u4e2a\u4efb\u52a1...\")\n                await asyncio.sleep(wait_sec)\n            else:\n                logger.info(\"\ud83c\udf89 \u6240\u6709\u4efb\u52a1\u5df2\u6309\u987a\u5e8f\u6267\u884c\u5b8c\u6bd5\uff01\")\n\n    except KeyboardInterrupt:\n        print(\"\\n\ud83d\udc4b \u7528\u6237\u624b\u52a8\u505c\u6b62\u7a0b\u5e8f\")\n    except Exception as e:\n        logger.error(f\"\ud83d\udd25 \u7cfb\u7edf\u81f4\u547d\u9519\u8bef: {e}\")\n    finally:\n        await client.disconnect()\n        logger.info(\"\ud83d\udd0c \u5df2\u5b89\u5168\u9000\u51fa\u5e76\u65ad\u5f00\u8fde\u63a5\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())<\/code><\/pre>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-28","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/wp.bao.ksapp.me\/index.php?rest_route=\/wp\/v2\/posts\/28","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wp.bao.ksapp.me\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wp.bao.ksapp.me\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wp.bao.ksapp.me\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/wp.bao.ksapp.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=28"}],"version-history":[{"count":0,"href":"https:\/\/wp.bao.ksapp.me\/index.php?rest_route=\/wp\/v2\/posts\/28\/revisions"}],"wp:attachment":[{"href":"https:\/\/wp.bao.ksapp.me\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=28"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wp.bao.ksapp.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=28"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wp.bao.ksapp.me\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=28"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}