commit b1a1f95e4e1e951f93ace4fee9ed528b6b113eb1 Author: Kakifilem Team Date: Sat Mar 7 03:33:40 2026 +0800 Initial commit - Telegram thumbnail uploader bot diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e7ed0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.env +sessions/ +temp/ +__pycache__/ +*.pyc +venv/ +.env.* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b9a0cd --- /dev/null +++ b/README.md @@ -0,0 +1,266 @@ +# Telegram Thumbnail Upload Bot + +Bot ini untuk **upload video ke Telegram bersama thumbnail secara automatik**. + +--- + +## Keperluan + +Pastikan komputer sudah install: + +Disarankan guna Python **3.12 atau 3.13** + +Python 3.13 +https://www.python.org/downloads/release/python-3130/ + +Python 3.12 +https://www.python.org/downloads/release/python-3120/ + +**FFmpeg** +https://ffmpeg.org/download.html + +### Add FFmpeg to PATH (Windows) + +Selepas download dan extract FFmpeg, pastikan folder `bin` dimasukkan ke dalam **System PATH**. + +Contoh lokasi: + +``` +C:\ffmpeg\bin +``` + +Kalau folder ini tidak dimasukkan ke PATH, command `ffmpeg` dan `ffprobe` tidak akan dikenali oleh sistem. + +Lepas install, cuba test: + +``` +ffmpeg -version +ffprobe -version +``` + +Kalau keluar version info maksudnya sudah OK. + +--- + +## Setup Telegram API + +Pergi ke: + +https://my.telegram.org + +Login Telegram → masuk **API development tools** + +Ambil: + +``` +API_ID +API_HASH +``` + +--- + +## Buat Bot + +Pergi ke: + +https://t.me/BotFather + +Run command: + +``` +/newbot +``` + +Lepas siap BotFather akan bagi: + +``` +BOT_TOKEN +``` + +--- + +## Setup `.env` + +Dalam folder bot ada file: + +``` +.env.example +``` + +Rename jadi: + +``` +.env +``` + +Lepas tu isi macam ni: + +``` +BOT_TOKEN= +API_ID= +API_HASH= +``` + +Contoh: + +``` +BOT_TOKEN=123456:ABCDEF +API_ID=123456 +API_HASH=abcdef1234567890 +``` + +Jangan share file `.env` ni dengan orang lain. + +--- + +## Jalankan Bot + +Double click: + +``` +Start.bat +``` + +Script ni akan: + +- create virtual environment +- install dependency +- start bot + +Kalau berjaya akan nampak: + +``` +Bot started successfully +``` + +Selepas bot berjalan, buka Telegram dan cuba command berikut untuk test: + +``` +/send_video +``` + +atau + +``` +/batch +``` + +Bot akan menggunakan sample file yang sudah disediakan dalam folder: + +``` +single/ +batch/ +``` + +## Test Bot (Sample Files) + +Repository ini sudah menyediakan **sample video dan thumbnail** supaya anda boleh terus test bot. + +--- + +## Upload Video (Single) + +Letak video dalam folder: + +``` +single/ +``` + +Contoh: + +``` +single/ + sample_video.mp4 + sample_video.jpg +``` + +Kemudian dalam Telegram hantar command: + +``` +/send_video +``` + +Bot akan upload semua video dalam folder tu. + +Pastikan **nama thumbnail sama dengan nama video**. + +Contoh: + +``` +sample_video.mp4 +sample_video.jpg +``` + +--- + +## Upload Drama (Batch) + +Struktur folder: + +``` +batch/ + +drama1/ + sample_video.mp4 + sample_video.jpg +``` + +Dalam Telegram run: + +``` +/batch +``` + +Bot akan upload semua video dalam setiap folder batch. + +--- + +## Nota + +Kali pertama bot run dia akan minta: + +``` +Phone number +OTP code +``` + +Ini untuk create **user session** supaya boleh upload video besar. + +Session akan disimpan dalam folder: + +``` +sessions/ +``` + +--- + +## Converter (Optional) + +Dalam folder `converter` ada tool kecil untuk convert image ke JPG kalau thumbnail format pelik macam: + +``` +AVIF +WEBP +HEIC +PNG +``` + +Run saja: + +``` +converter/Start.bat +``` + +atau + +``` +python converter/converter.py +``` + +Letakkan image dalam folder `converter/` dan hasil convert akan disimpan dalam folder `output_images/`. + +--- + +## Siap + +Lepas setup semua ni, bot dah boleh guna untuk upload video ke Telegram dengan senang. \ No newline at end of file diff --git a/Start.bat b/Start.bat new file mode 100644 index 0000000..79d868a --- /dev/null +++ b/Start.bat @@ -0,0 +1,37 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0" + +echo ============================== +echo Telegram Thumbnail Bot +echo ============================== + +REM +where python >nul 2>nul +if %errorlevel% neq 0 ( + echo Python tidak dijumpai. Sila install Python dahulu. + pause + exit +) + +REM +if not exist venv ( + echo First time setup detected... + echo Creating virtual environment... + python -m venv venv + + call venv\Scripts\activate.bat + + echo Updating pip... + python -m pip install --upgrade pip setuptools wheel + + echo Installing requirements... + pip install -r requirements.txt +) else ( + call venv\Scripts\activate.bat +) + +echo Starting bot... +python main.py + +pause \ No newline at end of file diff --git a/batch/drama1/sample_video.jpg b/batch/drama1/sample_video.jpg new file mode 100644 index 0000000..fd07650 Binary files /dev/null and b/batch/drama1/sample_video.jpg differ diff --git a/batch/drama1/sample_video.mp4 b/batch/drama1/sample_video.mp4 new file mode 100644 index 0000000..10e92c0 Binary files /dev/null and b/batch/drama1/sample_video.mp4 differ diff --git a/batch/drama2/sample_video.jpg b/batch/drama2/sample_video.jpg new file mode 100644 index 0000000..fd07650 Binary files /dev/null and b/batch/drama2/sample_video.jpg differ diff --git a/batch/drama2/sample_video.mp4 b/batch/drama2/sample_video.mp4 new file mode 100644 index 0000000..10e92c0 Binary files /dev/null and b/batch/drama2/sample_video.mp4 differ diff --git a/converter/Start.bat b/converter/Start.bat new file mode 100644 index 0000000..0f81a31 --- /dev/null +++ b/converter/Start.bat @@ -0,0 +1,26 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0" + +echo ============================== +echo Image Converter Tool +echo ============================== +echo. + +REM +cd .. + +REM +call venv\Scripts\activate.bat + +REM +cd converter + +echo Starting converter... +echo. + +python converter.py + +echo. +echo Conversion finished. +pause \ No newline at end of file diff --git a/converter/converter.py b/converter/converter.py new file mode 100644 index 0000000..a7cb93e --- /dev/null +++ b/converter/converter.py @@ -0,0 +1,53 @@ +import os +import imageio.v3 as iio +from PIL import Image + +input_dir = os.getcwd() +output_dir = os.path.join(input_dir, "output_images") +os.makedirs(output_dir, exist_ok=True) + +supported_extensions = { + ".avif", ".webp", ".png", ".heif", ".heic", + ".bmp", ".tiff" +} + +files = [ + f for f in os.listdir(input_dir) + if os.path.splitext(f)[1].lower() in supported_extensions +] + +total = len(files) +count = 0 + +print(f"Found {total} convertible images\n") + +for filename in files: + filepath = os.path.join(input_dir, filename) + name, ext = os.path.splitext(filename) + ext = ext.lower() + + count += 1 + + try: + if ext == ".avif": + img_array = iio.imread(filepath) + img = Image.fromarray(img_array).convert("RGB") + else: + img = Image.open(filepath).convert("RGB") + + output_path = os.path.join(output_dir, name + ".jpg") + + if os.path.exists(output_path): + print(f"[{count}/{total}] ⏭️ Skipped (exists): {name}.jpg") + continue + + img.thumbnail((320, 320)) + + img.save(output_path, "JPEG", quality=90, subsampling=0) + + print(f"[{count}/{total}] ✅ Converted: {filename} → {name}.jpg") + + except Exception as e: + print(f"[{count}/{total}] ❌ Failed: {filename} ({e})") + +print("\nConversion finished.") \ No newline at end of file diff --git a/converter/sample_converter_photo.avif b/converter/sample_converter_photo.avif new file mode 100644 index 0000000..a1c0abd Binary files /dev/null and b/converter/sample_converter_photo.avif differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..65c2d9a --- /dev/null +++ b/main.py @@ -0,0 +1,231 @@ +import os +import time +import logging +import subprocess +import json +from dotenv import load_dotenv +from pyrogram import Client, filters +from pyrogram.errors import FloodWait +import asyncio + +from utils import ( + get_local_videos, + process_local_thumbnail, + get_batch_directories, + get_batch_videos +) + +load_dotenv() + +BOT_TOKEN = os.getenv("BOT_TOKEN") +API_ID = int(os.getenv("API_ID")) +API_HASH = os.getenv("API_HASH") + +if not BOT_TOKEN or not API_ID or not API_HASH: + raise ValueError("Please configure BOT_TOKEN, API_ID and API_HASH in .env") + +USER_SESSION = "user_session" + +SESSION_DIR = os.path.join(os.path.dirname(__file__), "sessions") +os.makedirs(SESSION_DIR, exist_ok=True) + +TEMP_DIR = os.path.join(os.path.dirname(__file__), "temp") +os.makedirs(TEMP_DIR, exist_ok=True) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s" +) + +logger = logging.getLogger(__name__) + +Bot = Client( + os.path.join(SESSION_DIR, "Thumb-Bot"), + bot_token=BOT_TOKEN, + api_id=API_ID, + api_hash=API_HASH +) + +User = Client( + os.path.join(SESSION_DIR, USER_SESSION), + api_id=API_ID, + api_hash=API_HASH +) + + +def remove_extension(name: str) -> str: + return os.path.splitext(name)[0] + + +def get_video_dimensions(path: str): + cmd = [ + "ffprobe", "-v", "error", + "-select_streams", "v:0", + "-show_entries", "stream=width,height", + "-of", "json", + path + ] + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False + ) + + if not result.stdout: + return 1280, 720 + + data = json.loads(result.stdout) + streams = data.get("streams") + + if not streams: + return 1280, 720 + + width = streams[0].get("width", 1280) + height = streams[0].get("height", 720) + + return int(width), int(height) + + except Exception: + return 1280, 720 + + +def get_video_duration(path: str): + cmd = [ + "ffprobe", "-v", "error", + "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1", + path + ] + + try: + result = subprocess.run(cmd, capture_output=True, text=True) + return int(float(result.stdout.strip())) + except: + return 0 + +async def send_video_safe( + client, + chat_id, + video_path, + caption=None, + thumb=None +): + width, height = get_video_dimensions(video_path) + duration = get_video_duration(video_path) + + while True: + try: + await client.send_video( + chat_id=chat_id, + video=video_path, + caption=caption, + thumb=thumb, + width=width, + height=height, + duration=duration, + supports_streaming=True + ) + break + + except FloodWait as e: + logger.warning( + f"FloodWait while uploading {os.path.basename(video_path)}. " + f"Sleeping {e.value + 1} seconds..." + ) + await asyncio.sleep(e.value + 1) + +@Bot.on_message(filters.command("start")) +async def start(_, m): + await m.reply( + "Video uploader bot ready.\n\n" + "Commands:\n" + "/send_video - Upload videos from single folder\n" + "/batch - Upload videos from batch folders" + ) + + +@Bot.on_message(filters.command("send_video")) +async def send_local(_, m): + + status = await m.reply("Scanning local videos...") + + videos, _ = get_local_videos() + + if not videos: + await status.edit("No videos found.") + return + + for v in videos: + + msg = await m.reply(f"Uploading {v['filename']}...") + + thumb = None + if v["thumb_path"]: + thumb = process_local_thumbnail(v["thumb_path"]) + + await send_video_safe( + User, + m.chat.id, + v["video_path"], + caption=remove_extension(v["filename"]), + thumb=thumb + ) + + await msg.delete() + + if thumb and os.path.exists(thumb): + os.remove(thumb) + + await status.edit("All videos uploaded.") + + +@Bot.on_message(filters.command("batch")) +async def batch(_, m): + + status = await m.reply("Scanning batch folders...") + + batches = get_batch_directories() + + if not batches: + await status.edit("No batch folders found.") + return + + for batch_dir in batches: + + videos = get_batch_videos(batch_dir) + + if not videos: + continue + + thumb = None + + if videos[0]["thumb_path"]: + thumb = process_local_thumbnail(videos[0]["thumb_path"]) + + for v in videos: + + msg = await m.reply(f"Uploading {v['filename']}...") + + await send_video_safe( + User, + m.chat.id, + v["video_path"], + caption=remove_extension(v["filename"]), + thumb=thumb + ) + + await msg.delete() + + if thumb and os.path.exists(thumb): + os.remove(thumb) + + await status.edit("Batch upload complete.") + + +print("Bot started successfully") + +with User: + Bot.run() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7ea4655 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +pyrofork>=2.3.69 +TgCrypto-pyrofork==1.2.8 +humanize==4.15.0 +Pillow>=12.1.0 +python-dotenv==1.2.1 +pillow-heif +pillow-avif-plugin +imageio \ No newline at end of file diff --git a/single/sample_video.jpg b/single/sample_video.jpg new file mode 100644 index 0000000..fd07650 Binary files /dev/null and b/single/sample_video.jpg differ diff --git a/single/sample_video.mp4 b/single/sample_video.mp4 new file mode 100644 index 0000000..10e92c0 Binary files /dev/null and b/single/sample_video.mp4 differ diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..eea76b0 --- /dev/null +++ b/utils.py @@ -0,0 +1,212 @@ +import os +import re +import time +import logging +from pathlib import Path +from PIL import Image + +logger = logging.getLogger(__name__) + +BASE_DIR = os.path.dirname(__file__) +SINGLE_DIR = os.path.join(BASE_DIR, "single") +BATCH_DIR = os.path.join(BASE_DIR, "batch") + +TEMP_DIR = os.path.join(BASE_DIR, "temp") +os.makedirs(TEMP_DIR, exist_ok=True) + +VIDEO_FORMATS = ( + ".mp4", ".mkv", ".webm", ".avi", ".mov", + ".m4v", ".flv", ".wmv", ".ts", + ".mpg", ".mpeg", ".3gp", ".mp4v", ".vob" +) + +THUMB_FORMATS = (".jpg", ".jpeg", ".png") + +def find_matching_thumbnail(video_path): + video_name = Path(video_path).stem.lower() + + # Exact match + for ext in THUMB_FORMATS: + exact_match = os.path.join(SINGLE_DIR, f"{video_name}{ext}") + if os.path.exists(exact_match): + logger.info(f"Exact thumbnail found: {exact_match}") + return exact_match + + video_keywords = video_name.split(".") + + for file in os.listdir(SINGLE_DIR): + if file.lower().endswith(THUMB_FORMATS): + + thumb_name = Path(file).stem.lower() + + if ( + thumb_name in video_name + or video_name in thumb_name + or any(keyword in thumb_name for keyword in video_keywords) + ): + return os.path.join(SINGLE_DIR, file) + + return None + + +def get_local_videos(): + video_files = [] + + logger.info(f"Scanning single folder: {SINGLE_DIR}") + + videos = [ + f for f in os.listdir(SINGLE_DIR) + if f.lower().endswith(VIDEO_FORMATS) + ] + + videos.sort() + + for video in videos: + + video_path = os.path.join(SINGLE_DIR, video) + + thumb_path = find_matching_thumbnail(video_path) + + video_files.append({ + "video_path": video_path, + "thumb_path": thumb_path, + "filename": video, + "size": os.path.getsize(video_path), + "has_thumb": thumb_path is not None + }) + + logger.info( + f"Found video: {video} {'with thumbnail' if thumb_path else 'no thumbnail'}" + ) + + return video_files, {} + +def process_local_thumbnail(thumb_path): + + try: + img = Image.open(thumb_path).convert("RGB") + + width, height = img.size + + if width > height: + new_width = 320 + new_height = int(320 * height / width) + else: + new_height = 320 + new_width = int(320 * width / height) + + img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) + + output_path = os.path.join( + TEMP_DIR, + f"thumb_{int(time.time())}.jpg" + ) + + img.save(output_path, "JPEG", quality=95) + + return output_path + + except Exception as e: + logger.error(f"Thumbnail processing error: {e}") + return None + +def find_batch_thumbnail(batch_dir): + + for file in os.listdir(batch_dir): + if file.lower().endswith(THUMB_FORMATS): + return os.path.join(batch_dir, file) + + return None + + +def extract_episode_number(filename): + + patterns = [ + r'[Ss](\d+)[Ee](\d+)', + r'[Ee][Pp]?\.?\s*(\d+)', + r'E(\d+)', + r'(\d+)' + ] + + filename = filename.lower() + + for pattern in patterns: + + match = re.search(pattern, filename) + + if match: + try: + if len(match.groups()) == 2: + return int(match.group(2)) + + return int(match.group(1)) + + except: + continue + + return 0 + + +def get_batch_videos(batch_dir): + + video_files = [] + + thumb_path = find_batch_thumbnail(batch_dir) + + videos = [] + + for f in os.listdir(batch_dir): + + if f.lower().endswith(VIDEO_FORMATS): + + ep = extract_episode_number(f) + + videos.append((f, ep)) + + videos.sort(key=lambda x: x[1]) + + for video, ep_num in videos: + + video_path = os.path.join(batch_dir, video) + + video_files.append({ + "video_path": video_path, + "thumb_path": thumb_path, + "filename": video, + "episode": ep_num, + "size": os.path.getsize(video_path) + }) + + logger.info(f"Found video {video} episode {ep_num}") + + return video_files + +def natural_sort_key(s): + return [ + int(text) if text.isdigit() else text.lower() + for text in re.split(r'([0-9]+)', s) + ] + +def get_batch_directories(): + + batch_dirs = [] + + if not os.path.exists(BATCH_DIR): + return batch_dirs + + has_root_videos = False + + for item in sorted(os.listdir(BATCH_DIR), key=natural_sort_key): + + full = os.path.join(BATCH_DIR, item) + + if os.path.isdir(full): + batch_dirs.append(full) + + elif item.lower().endswith(VIDEO_FORMATS): + has_root_videos = True + + if has_root_videos: + batch_dirs.insert(0, BATCH_DIR) + + return batch_dirs \ No newline at end of file