Import source: bot

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:16:05 +03:30
parent 864404b788
commit aca4422310
+427
View File
@@ -0,0 +1,427 @@
import asyncio
import logging
import os
import shutil
import sqlite3
import subprocess
import time
import uuid
from pathlib import Path
from typing import Optional, List, Tuple
from dotenv import load_dotenv
from PIL import Image, ImageOps, UnidentifiedImageError
from telegram import Update, InputMediaPhoto
from telegram.ext import (
Application,
ApplicationBuilder,
CommandHandler,
ContextTypes,
MessageHandler,
filters,
)
load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN", "").strip()
FFMPEG_PATH = os.getenv("FFMPEG_PATH", "/usr/bin/ffmpeg").strip()
MAX_MEDIA_PER_FINAL = int(os.getenv("MAX_MEDIA_PER_FINAL", "200"))
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
DOWNLOAD_DIR = BASE_DIR / "downloads"
OUTPUT_DIR = BASE_DIR / "output"
DB_PATH = DATA_DIR / "queue.sqlite3"
for directory in [DATA_DIR, DOWNLOAD_DIR, OUTPUT_DIR]:
directory.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logger = logging.getLogger("media_to_photo_bot")
def db() -> sqlite3.Connection:
conn = sqlite3.connect(DB_PATH)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS media_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
file_id TEXT NOT NULL,
media_type TEXT NOT NULL,
file_name TEXT,
mime_type TEXT,
created_at INTEGER NOT NULL
)
"""
)
conn.commit()
return conn
def add_to_queue(
chat_id: int,
user_id: int,
file_id: str,
media_type: str,
file_name: Optional[str],
mime_type: Optional[str],
) -> int:
conn = db()
cur = conn.cursor()
cur.execute(
"""
INSERT INTO media_queue
(chat_id, user_id, file_id, media_type, file_name, mime_type, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
chat_id,
user_id,
file_id,
media_type,
file_name,
mime_type,
int(time.time()),
),
)
conn.commit()
count = conn.execute(
"SELECT COUNT(*) FROM media_queue WHERE chat_id = ? AND user_id = ?",
(chat_id, user_id),
).fetchone()[0]
conn.close()
return count
def get_queue(chat_id: int, user_id: int) -> List[Tuple]:
conn = db()
rows = conn.execute(
"""
SELECT id, file_id, media_type, file_name, mime_type
FROM media_queue
WHERE chat_id = ? AND user_id = ?
ORDER BY id ASC
LIMIT ?
""",
(chat_id, user_id, MAX_MEDIA_PER_FINAL),
).fetchall()
conn.close()
return rows
def clear_queue(chat_id: int, user_id: int) -> int:
conn = db()
cur = conn.cursor()
cur.execute(
"DELETE FROM media_queue WHERE chat_id = ? AND user_id = ?",
(chat_id, user_id),
)
deleted = cur.rowcount
conn.commit()
conn.close()
return deleted
def safe_suffix(file_name: Optional[str], fallback: str) -> str:
if not file_name:
return fallback
suffix = Path(file_name).suffix.lower()
if not suffix:
return fallback
if len(suffix) > 10:
return fallback
return suffix
def ffprobe_path() -> str:
ffmpeg = Path(FFMPEG_PATH)
possible = ffmpeg.with_name("ffprobe")
return str(possible) if possible.exists() else "ffprobe"
def get_video_duration(path: Path) -> Optional[float]:
try:
result = subprocess.run(
[
ffprobe_path(),
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
str(path),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=30,
)
if result.returncode != 0:
return None
return float(result.stdout.strip())
except Exception:
return None
def convert_image_to_jpg(input_path: Path, output_path: Path) -> bool:
try:
with Image.open(input_path) as img:
img = ImageOps.exif_transpose(img)
if img.mode in ("RGBA", "LA", "P"):
img = img.convert("RGBA")
background = Image.new("RGBA", img.size, (255, 255, 255, 255))
background.alpha_composite(img)
img = background.convert("RGB")
else:
img = img.convert("RGB")
img.save(output_path, "JPEG", quality=92, optimize=True)
return True
except UnidentifiedImageError:
return False
except Exception as e:
logger.exception("Image conversion failed: %s", e)
return False
def convert_video_to_jpg(input_path: Path, output_path: Path) -> bool:
duration = get_video_duration(input_path)
seek_time = 1
if duration and duration > 3:
seek_time = max(1, int(duration / 2))
try:
result = subprocess.run(
[
FFMPEG_PATH,
"-y",
"-ss",
str(seek_time),
"-i",
str(input_path),
"-frames:v",
"1",
"-q:v",
"2",
str(output_path),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=120,
)
return result.returncode == 0 and output_path.exists() and output_path.stat().st_size > 0
except Exception as e:
logger.exception("Video conversion failed: %s", e)
return False
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
text = (
"بات آماده است.\n\n"
"مدیا بفرست: عکس، ویدیو، GIF، یا فایل image/video.\n\n"
"دستورها:\n"
"/queue - دیدن تعداد فایل‌های صف\n"
"/final - دانلود همه مدیاها، تبدیل به عکس، ارسال خروجی\n"
"/clear - پاک کردن صف"
)
await update.effective_message.reply_text(text)
async def queue_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat_id = update.effective_chat.id
user_id = update.effective_user.id
rows = get_queue(chat_id, user_id)
await update.effective_message.reply_text(f"تعداد مدیاهای داخل صف: {len(rows)}")
async def clear(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat_id = update.effective_chat.id
user_id = update.effective_user.id
deleted = clear_queue(chat_id, user_id)
await update.effective_message.reply_text(f"صف پاک شد. تعداد حذف‌شده: {deleted}")
async def collect_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
msg = update.effective_message
chat_id = update.effective_chat.id
user_id = update.effective_user.id
file_id = None
media_type = None
file_name = None
mime_type = None
if msg.photo:
best_photo = msg.photo[-1]
file_id = best_photo.file_id
media_type = "image"
file_name = f"telegram_photo_{best_photo.file_unique_id}.jpg"
mime_type = "image/jpeg"
elif msg.video:
file_id = msg.video.file_id
media_type = "video"
file_name = msg.video.file_name or f"telegram_video_{msg.video.file_unique_id}.mp4"
mime_type = msg.video.mime_type or "video/mp4"
elif msg.animation:
file_id = msg.animation.file_id
media_type = "video"
file_name = msg.animation.file_name or f"telegram_animation_{msg.animation.file_unique_id}.mp4"
mime_type = msg.animation.mime_type or "video/mp4"
elif msg.document:
doc = msg.document
file_id = doc.file_id
file_name = doc.file_name or f"telegram_document_{doc.file_unique_id}"
mime_type = doc.mime_type or ""
if mime_type.startswith("image/"):
media_type = "image"
elif mime_type.startswith("video/"):
media_type = "video"
else:
await msg.reply_text("این فایل image/video نیست؛ داخل صف اضافه نشد.")
return
else:
await msg.reply_text("فقط عکس، ویدیو، GIF یا فایل image/video پشتیبانی می‌شود.")
return
count = add_to_queue(
chat_id=chat_id,
user_id=user_id,
file_id=file_id,
media_type=media_type,
file_name=file_name,
mime_type=mime_type,
)
await msg.reply_text(f"اضافه شد. تعداد فعلی صف: {count}\nبرای اجرای نهایی: /final")
async def final(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
msg = update.effective_message
chat_id = update.effective_chat.id
user_id = update.effective_user.id
rows = get_queue(chat_id, user_id)
if not rows:
await msg.reply_text("صف خالی است. اول چند مدیا بفرست.")
return
job_id = uuid.uuid4().hex[:12]
job_download_dir = DOWNLOAD_DIR / f"{chat_id}_{user_id}_{job_id}"
job_output_dir = OUTPUT_DIR / f"{chat_id}_{user_id}_{job_id}"
job_download_dir.mkdir(parents=True, exist_ok=True)
job_output_dir.mkdir(parents=True, exist_ok=True)
status = await msg.reply_text(f"شروع شد. تعداد فایل‌ها: {len(rows)}")
output_images: List[Path] = []
failed = 0
try:
for index, row in enumerate(rows, start=1):
queue_id, file_id, media_type, file_name, mime_type = row
await status.edit_text(f"درحال دانلود و تبدیل: {index}/{len(rows)}")
tg_file = await context.bot.get_file(file_id)
suffix = safe_suffix(file_name, ".bin")
input_path = job_download_dir / f"{queue_id}{suffix}"
output_path = job_output_dir / f"{queue_id}.jpg"
await tg_file.download_to_drive(custom_path=str(input_path))
ok = False
if media_type == "image":
ok = await asyncio.to_thread(convert_image_to_jpg, input_path, output_path)
elif media_type == "video":
ok = await asyncio.to_thread(convert_video_to_jpg, input_path, output_path)
if ok:
output_images.append(output_path)
else:
failed += 1
if not output_images:
await status.edit_text("هیچ خروجی تصویری ساخته نشد.")
return
await status.edit_text(f"تبدیل تمام شد. درحال ارسال {len(output_images)} عکس...")
for i in range(0, len(output_images), 10):
chunk = output_images[i:i + 10]
media_group = []
opened_files = []
try:
for image_path in chunk:
f = open(image_path, "rb")
opened_files.append(f)
media_group.append(InputMediaPhoto(media=f))
await context.bot.send_media_group(
chat_id=chat_id,
media=media_group,
reply_to_message_id=msg.message_id,
)
finally:
for f in opened_files:
f.close()
clear_queue(chat_id, user_id)
done_text = f"تمام شد. خروجی‌های ارسال‌شده: {len(output_images)}"
if failed:
done_text += f"\nناموفق: {failed}"
await status.edit_text(done_text)
except Exception as e:
logger.exception("Final command failed: %s", e)
await status.edit_text(f"خطا هنگام پردازش: {e}")
finally:
shutil.rmtree(job_download_dir, ignore_errors=True)
shutil.rmtree(job_output_dir, ignore_errors=True)
def main() -> None:
if not BOT_TOKEN:
raise RuntimeError("BOT_TOKEN is missing in .env")
application: Application = ApplicationBuilder().token(BOT_TOKEN).build()
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("queue", queue_status))
application.add_handler(CommandHandler("clear", clear))
application.add_handler(CommandHandler("final", final))
media_filter = (
filters.PHOTO
| filters.VIDEO
| filters.ANIMATION
| filters.Document.IMAGE
| filters.Document.VIDEO
)
application.add_handler(MessageHandler(media_filter, collect_media))
logger.info("Bot started.")
application.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()