Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c49e080d41 | |||
| 089e90b36a | |||
| 5b0152e47c | |||
| 817819c382 | |||
| 525d52cc76 | |||
| c7ef7cdd57 | |||
| 7a2e73939f | |||
| effc2044f4 | |||
| 6042be7343 | |||
| 16b1d1e0a2 | |||
| 705acccf8f | |||
| 493bc0baeb | |||
| c1fa52fa33 | |||
| a1c257695e | |||
| 111f40d67a |
178
.gitignore
vendored
178
.gitignore
vendored
@@ -1,5 +1,177 @@
|
||||
cookies.txt
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sbtarget/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
*.mp4
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# Медиа файлы
|
||||
*.mp3
|
||||
wireguard/wg_confs/wg0.conf
|
||||
*.mp4
|
||||
*.mp4.info.json
|
||||
*.mp3.info.json
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.png
|
||||
*.gif
|
||||
*.mov
|
||||
*.avi
|
||||
*.mkv
|
||||
*.webm
|
||||
*.flv
|
||||
|
||||
# Кэш и временные файлы бота
|
||||
/cache/
|
||||
/tmp/
|
||||
downloads/
|
||||
*.cache
|
||||
*.tmp
|
||||
|
||||
# Конфигурационные файлы с чувствительными данными
|
||||
config.py
|
||||
secrets.py
|
||||
cookies.txt
|
||||
*.key
|
||||
*.pem
|
||||
*.crt
|
||||
|
||||
# Логи
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Операционные системы
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Редакторы
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
**.wg0.conf
|
||||
0
CHANGELOG.md
Normal file
0
CHANGELOG.md
Normal file
14
bot/cache.py
14
bot/cache.py
@@ -1,9 +1,13 @@
|
||||
# cache.py
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
def cache_key(url, quality, audio=False):
|
||||
raw = f"{url}:{quality}:{audio}"
|
||||
return hashlib.sha256(raw.encode()).hexdigest()
|
||||
def cache_key(url: str, quality: str, audio: bool = False) -> str:
|
||||
key_str = f"{url}_{quality}_{audio}"
|
||||
return hashlib.sha256(key_str.encode()).hexdigest()
|
||||
|
||||
def cache_path(cache_dir, key, ext):
|
||||
return os.path.join(cache_dir, f"{key}.{ext}")
|
||||
def cache_path(cache_dir: str, key: str, extension: str) -> str:
|
||||
subdir = key[:2]
|
||||
full_dir = os.path.join(cache_dir, subdir)
|
||||
os.makedirs(full_dir, exist_ok=True)
|
||||
return os.path.join(full_dir, f"{key}.{extension}")
|
||||
@@ -1,3 +1,4 @@
|
||||
# config.py
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
@@ -19,8 +20,14 @@ COOKIES_FILE = os.getenv("COOKIES_FILE")
|
||||
MAX_DURATION_SECONDS = int(os.getenv("MAX_DURATION_SECONDS", 1800))
|
||||
RATE_LIMIT_SECONDS = int(os.getenv("RATE_LIMIT_SECONDS", 20))
|
||||
MAX_FILE_SIZE_MB = 2000
|
||||
CACHE_MAX_AGE_DAYS = 7 # Удалять файлы старше 7 дней
|
||||
CACHE_MAX_SIZE_MB = 4096 # Максимальный размер кэша 1 ГБ (0 = без ограничения)
|
||||
CACHE_MAX_AGE_DAYS = 7
|
||||
CACHE_MAX_SIZE_MB = 4096
|
||||
|
||||
# Авто-качества для TikTok и Instagram (без выбора)
|
||||
DEFAULT_TIKTOK_VIDEO_QUALITY = "1080"
|
||||
DEFAULT_TIKTOK_AUDIO_QUALITY = "192" # кбит/с
|
||||
DEFAULT_INSTAGRAM_VIDEO_QUALITY = "1080"
|
||||
|
||||
# Private mode
|
||||
ALLOWED_USERS = set(
|
||||
int(uid.strip())
|
||||
@@ -30,4 +37,4 @@ ALLOWED_USERS = set(
|
||||
|
||||
# Ensure directories exist
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
os.makedirs(TMP_DIR, exist_ok=True)
|
||||
os.makedirs(TMP_DIR, exist_ok=True)
|
||||
@@ -1,28 +1,60 @@
|
||||
# downloader.py
|
||||
import yt_dlp
|
||||
import threading
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
import shutil
|
||||
import asyncio
|
||||
from typing import List
|
||||
|
||||
from config import (
|
||||
DEFAULT_TIKTOK_VIDEO_QUALITY,
|
||||
DEFAULT_TIKTOK_AUDIO_QUALITY,
|
||||
DEFAULT_INSTAGRAM_VIDEO_QUALITY,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DownloadCancelled(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _progress_hook(cancel_event, progress_cb):
|
||||
def hook(d):
|
||||
if cancel_event.is_set():
|
||||
raise DownloadCancelled("Cancelled by user")
|
||||
|
||||
if d["status"] == "downloading":
|
||||
if d["status"] in ["downloading", "finished"] and progress_cb:
|
||||
progress_cb(d)
|
||||
|
||||
return hook
|
||||
|
||||
def optimize_for_telegram(input_path: str, output_path: str) -> None:
|
||||
"""
|
||||
Синхронная функция: конвертирует видео в Telegram-safe mp4
|
||||
"""
|
||||
file_size_mb = os.path.getsize(input_path) / (1024 * 1024)
|
||||
crf = 28 if file_size_mb > 50 else 23
|
||||
|
||||
# ---------------- VIDEO ----------------
|
||||
cmd = [
|
||||
'ffmpeg',
|
||||
'-i', input_path,
|
||||
'-c:v', 'libx264',
|
||||
'-preset', 'fast',
|
||||
'-crf', str(crf),
|
||||
'-c:a', 'aac',
|
||||
'-b:a', '128k',
|
||||
'-movflags', '+faststart',
|
||||
'-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
|
||||
'-y',
|
||||
output_path
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
||||
if result.returncode != 0:
|
||||
logger.error(f"FFmpeg error: {result.stderr}")
|
||||
shutil.copy2(input_path, output_path)
|
||||
|
||||
|
||||
# ---------------- YOUTUBE/VK: Video (с выбором качества) ------
|
||||
def download_video(
|
||||
url: str,
|
||||
quality: str,
|
||||
@@ -32,27 +64,42 @@ def download_video(
|
||||
progress_cb,
|
||||
):
|
||||
ydl_opts = {
|
||||
"format": quality,
|
||||
"format": f"best[height<={quality}]/best",
|
||||
"outtmpl": out_path,
|
||||
"merge_output_format": "mp4",
|
||||
"cookiefile": cookies,
|
||||
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
||||
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
# Настройки для TikTok
|
||||
"extractor_args": {
|
||||
"tiktok": {
|
||||
"skip_impersonation": True # Пропускаем имитацию если зависимости не установлены
|
||||
}
|
||||
},
|
||||
"concurrent_fragment_downloads": 4,
|
||||
"http_chunk_size": 10485760,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
|
||||
# Ищем реальный файл
|
||||
base = out_path.rsplit('.', 1)[0]
|
||||
actual_video = None
|
||||
for ext in ['', '.mp4', '.mov', '.webm', '.mkv']:
|
||||
candidate = base + ext
|
||||
if os.path.exists(candidate):
|
||||
actual_video = candidate
|
||||
break
|
||||
|
||||
# ---------------- AUDIO FROM VIDEO ----------------
|
||||
if not actual_video:
|
||||
raise Exception(f"Video file not created for {url}")
|
||||
|
||||
# Конвертируем в Telegram-safe mp4 (синхронно)
|
||||
optimized_path = base + "_telegram.mp4"
|
||||
optimize_for_telegram(actual_video, optimized_path) # ← без await!
|
||||
|
||||
# Удаляем старый файл и ставим конвертированный
|
||||
for path in [out_path, actual_video]:
|
||||
if os.path.exists(path): os.remove(path)
|
||||
os.rename(optimized_path, out_path)
|
||||
|
||||
|
||||
# ---------------- AUDIO (YouTube/VK) ------
|
||||
def download_audio(
|
||||
url: str,
|
||||
out_path: str,
|
||||
@@ -63,27 +110,213 @@ def download_audio(
|
||||
ydl_opts = {
|
||||
"format": "bestaudio/best",
|
||||
"outtmpl": out_path.replace('.mp3', ''),
|
||||
"cookiefile": cookies,
|
||||
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
||||
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
|
||||
"postprocessors": [
|
||||
{
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": "mp3",
|
||||
"preferredquality": "320",
|
||||
},
|
||||
{"key": "FFmpegMetadata"},
|
||||
{"key": "EmbedThumbnail"},
|
||||
],
|
||||
"writethumbnail": True,
|
||||
"postprocessors": [{
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": "mp3",
|
||||
"preferredquality": "192",
|
||||
}],
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
# Настройки для TikTok
|
||||
"extractor_args": {
|
||||
"tiktok": {
|
||||
"skip_impersonation": True
|
||||
}
|
||||
},
|
||||
"concurrent_fragment_downloads": 4,
|
||||
"http_chunk_size": 10485760,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
ydl.download([url])
|
||||
|
||||
# Ищем файл
|
||||
base = out_path.rsplit('.', 1)[0]
|
||||
actual_audio = None
|
||||
for ext in ['', '.mp3', '.m4a', '.opus', '.weba']:
|
||||
candidate = base + ext
|
||||
if os.path.exists(candidate):
|
||||
actual_audio = candidate
|
||||
break
|
||||
|
||||
if not actual_audio:
|
||||
raise Exception(f"Audio file not created for {url}")
|
||||
|
||||
# Конвертируем в mp3
|
||||
if actual_audio != out_path:
|
||||
optimized = base + "_telegram.mp3"
|
||||
optimize_for_telegram_audio(actual_audio, optimized) # ← отдельная функция
|
||||
if os.path.exists(out_path): os.remove(out_path)
|
||||
os.rename(optimized, out_path)
|
||||
|
||||
|
||||
# ---------------- TikTok: Video + Audio (автоматически) ------
|
||||
def download_tiktok_video_and_audio(
|
||||
url: str,
|
||||
video_path: str,
|
||||
audio_path: str,
|
||||
cookies: str | None,
|
||||
cancel_event: threading.Event,
|
||||
progress_cb,
|
||||
):
|
||||
# Скачиваем видео
|
||||
video_opts = {
|
||||
"format": f"bestvideo[height<={DEFAULT_TIKTOK_VIDEO_QUALITY}]+bestaudio/best",
|
||||
"outtmpl": video_path,
|
||||
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
||||
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
|
||||
"quiet": True,
|
||||
"no_warnings": False,
|
||||
"concurrent_fragment_downloads": 4,
|
||||
"http_chunk_size": 10485760,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(video_opts) as ydl:
|
||||
ydl.download([url])
|
||||
|
||||
# Ищем видеофайл
|
||||
base_video = video_path.rsplit('.', 1)[0]
|
||||
actual_video = None
|
||||
for ext in ['', '.mp4', '.mov', '.webm']:
|
||||
candidate = base_video + ext
|
||||
if os.path.exists(candidate):
|
||||
actual_video = candidate
|
||||
break
|
||||
if not actual_video:
|
||||
raise Exception("TikTok video file not created")
|
||||
|
||||
# Конвертируем в Telegram-safe mp4
|
||||
optimized_video = base_video + "_telegram.mp4"
|
||||
optimize_for_telegram(actual_video, optimized_video)
|
||||
for path in [video_path, actual_video]:
|
||||
if os.path.exists(path): os.remove(path)
|
||||
os.rename(optimized_video, video_path)
|
||||
|
||||
# Аудио
|
||||
audio_opts = {
|
||||
"format": "bestaudio/best",
|
||||
"outtmpl": audio_path.replace('.mp3', ''),
|
||||
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
||||
"postprocessors": [{
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": "mp3",
|
||||
"preferredquality": DEFAULT_TIKTOK_AUDIO_QUALITY,
|
||||
}],
|
||||
"quiet": True,
|
||||
"no_warnings": False,
|
||||
"concurrent_fragment_downloads": 4,
|
||||
"http_chunk_size": 10485760,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(audio_opts) as ydl:
|
||||
ydl.download([url])
|
||||
|
||||
base_audio = audio_path.rsplit('.', 1)[0]
|
||||
actual_audio = None
|
||||
for ext in ['', '.mp3', '.m4a', '.opus']:
|
||||
candidate = base_audio + ext
|
||||
if os.path.exists(candidate):
|
||||
actual_audio = candidate
|
||||
break
|
||||
if not actual_audio:
|
||||
raise Exception("TikTok audio file not created")
|
||||
|
||||
if actual_audio != audio_path:
|
||||
optimized_audio = base_audio + "_telegram.mp3"
|
||||
optimize_for_telegram_audio(actual_audio, optimized_audio)
|
||||
if os.path.exists(audio_path): os.remove(audio_path)
|
||||
os.rename(optimized_audio, audio_path)
|
||||
|
||||
|
||||
# ---------------- Instagram: Video (1080p) ------
|
||||
def download_instagram_video(
|
||||
url: str,
|
||||
out_path: str,
|
||||
cookies: str | None,
|
||||
cancel_event: threading.Event,
|
||||
progress_cb,
|
||||
):
|
||||
ydl_opts = {
|
||||
"format": f"bestvideo[height<={DEFAULT_INSTAGRAM_VIDEO_QUALITY}]+bestaudio/best",
|
||||
"outtmpl": out_path,
|
||||
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
||||
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
|
||||
"quiet": True,
|
||||
"no_warnings": False,
|
||||
"concurrent_fragment_downloads": 4,
|
||||
"http_chunk_size": 10485760,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
|
||||
base = out_path.rsplit('.', 1)[0]
|
||||
actual = None
|
||||
for ext in ['', '.mp4', '.mov', '.webm']:
|
||||
candidate = base + ext
|
||||
if os.path.exists(candidate):
|
||||
actual = candidate
|
||||
break
|
||||
if not actual:
|
||||
raise Exception("Instagram video not created")
|
||||
|
||||
optimized = base + "_telegram.mp4"
|
||||
optimize_for_telegram(actual, optimized)
|
||||
if os.path.exists(out_path): os.remove(out_path)
|
||||
os.rename(optimized, out_path)
|
||||
|
||||
|
||||
# ---------------- Audio helper (синхронная) ------
|
||||
def optimize_for_telegram_audio(input_path: str, output_path: str) -> None:
|
||||
"""Конвертирует аудио в Telegram-safe mp3"""
|
||||
cmd = [
|
||||
'ffmpeg',
|
||||
'-i', input_path,
|
||||
'-c:a', 'aac',
|
||||
'-b:a', '192k',
|
||||
'-loglevel', 'error',
|
||||
'-y',
|
||||
output_path
|
||||
]
|
||||
subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
||||
|
||||
|
||||
# ---------------- Playlist ------
|
||||
def download_playlist_videos(
|
||||
playlist_info: dict,
|
||||
output_dir: str,
|
||||
cookies: str | None,
|
||||
cancel_event: threading.Event,
|
||||
progress_cb,
|
||||
) -> List[str]:
|
||||
downloaded_files = []
|
||||
ydl_opts = {
|
||||
"format": "best[height<=1080]/best",
|
||||
"outtmpl": os.path.join(output_dir, "%(title)s [%(id)s].%(ext)s"),
|
||||
"cookiefile": cookies if cookies and os.path.exists(cookies) else None,
|
||||
"progress_hooks": [_progress_hook(cancel_event, progress_cb)],
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"ignoreerrors": True,
|
||||
"extract_flat": False,
|
||||
"nooverwrites": True,
|
||||
"concurrent_fragment_downloads": 4,
|
||||
"http_chunk_size": 10485760,
|
||||
}
|
||||
|
||||
try:
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
entries = playlist_info.get('entries', [])
|
||||
for i, entry in enumerate(entries, 1):
|
||||
if cancel_event.is_set():
|
||||
raise DownloadCancelled("Cancelled by user")
|
||||
if not entry.get('url'):
|
||||
continue
|
||||
try:
|
||||
info = ydl.extract_info(entry['url'], download=True)
|
||||
if info:
|
||||
filename = ydl.prepare_filename(info)
|
||||
if os.path.exists(filename):
|
||||
downloaded_files.append(filename)
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading video {i}: {e}")
|
||||
return downloaded_files
|
||||
except Exception as e:
|
||||
logger.error(f"Playlist download error: {e}")
|
||||
raise
|
||||
95
bot/info.py
95
bot/info.py
@@ -1,12 +1,93 @@
|
||||
import yt_dlp
|
||||
import re
|
||||
|
||||
def extract_info(url, cookies):
|
||||
opts = {
|
||||
|
||||
def extract_info(url: str, cookies_file: str = None) -> dict:
|
||||
"""Извлекает информацию о видео"""
|
||||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"skip_download": True,
|
||||
"no_warnings": True,
|
||||
"cookiefile": cookies_file,
|
||||
"extractor_args": {
|
||||
"tiktok": {"skip_impersonation": True},
|
||||
"instagram": {"skip_impersonation": True},
|
||||
},
|
||||
}
|
||||
if cookies:
|
||||
opts["cookiefile"] = cookies
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
try:
|
||||
return ydl.extract_info(url, download=False)
|
||||
except:
|
||||
return None
|
||||
|
||||
with yt_dlp.YoutubeDL(opts) as ydl:
|
||||
return ydl.extract_info(url, download=False)
|
||||
|
||||
def is_playlist(url: str) -> bool:
|
||||
"""Проверяет, является ли ссылка плейлистом"""
|
||||
# Паттерны для плейлистов
|
||||
playlist_patterns = [
|
||||
r"youtube\.com/playlist", # YouTube плейлист
|
||||
r"youtube\.com/watch.*list=", # YouTube видео из плейлиста
|
||||
r"youtu\.be/.*list=", # Сокращенная ссылка YouTube
|
||||
]
|
||||
|
||||
for pattern in playlist_patterns:
|
||||
if re.search(pattern, url, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
# Используем yt-dlp для более точной проверки
|
||||
try:
|
||||
ydl_opts = {"quiet": True, "no_warnings": True}
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(url, download=False, process=False)
|
||||
return info.get('_type') == 'playlist'
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def get_platform_info(url: str) -> str:
|
||||
"""Определяет платформу по ссылке"""
|
||||
patterns = {
|
||||
"youtube": r"(youtube\.com|youtu\.be)",
|
||||
"tiktok": r"tiktok\.com",
|
||||
"instagram": r"(instagram\.com|instagr\.am)",
|
||||
"twitter": r"(twitter\.com|x\.com)",
|
||||
"facebook": r"facebook\.com",
|
||||
"vk": r"vk\.com",
|
||||
"rutube": r"rutube\.ru",
|
||||
"vimeo": r"vimeo\.com",
|
||||
"twitch": r"twitch\.tv",
|
||||
"dailymotion": r"dailymotion\.com",
|
||||
}
|
||||
|
||||
for platform, pattern in patterns.items():
|
||||
if re.search(pattern, url, re.IGNORECASE):
|
||||
return platform
|
||||
|
||||
return "other"
|
||||
|
||||
|
||||
def get_playlist_info(url: str) -> dict:
|
||||
"""Получает информацию о плейлисте"""
|
||||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"extract_flat": True,
|
||||
"playlistend": 50, # Ограничиваем количество видео (можно изменить)
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
try:
|
||||
return ydl.extract_info(url, download=False)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_first_video_from_playlist(url: str) -> str:
|
||||
"""Получает первую ссылку на видео из плейлиста"""
|
||||
playlist_info = get_playlist_info(url)
|
||||
|
||||
if playlist_info and playlist_info.get('entries'):
|
||||
first_video = playlist_info['entries'][0]
|
||||
return first_video.get('url')
|
||||
|
||||
return None
|
||||
@@ -1,28 +1,50 @@
|
||||
# keyboards.py
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
|
||||
def quality_keyboard():
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="480p", callback_data="q:best[height<=480]"),
|
||||
InlineKeyboardButton(text="720p", callback_data="q:best[height<=720]"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="1080p", callback_data="q:best[height<=1080]"),
|
||||
InlineKeyboardButton(text="4K", callback_data="q:bestvideo+bestaudio"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🎧 Аудио из видео", callback_data="audio"),
|
||||
]
|
||||
def youtube_quality_keyboard():
|
||||
"""YouTube/VK: выбор качества"""
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton(text="📹 480p", callback_data="q:480"),
|
||||
InlineKeyboardButton(text="📹 720p", callback_data="q:720"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🎬 1080p", callback_data="q:1080"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🎧 Аудио", callback_data="audio"),
|
||||
InlineKeyboardButton(text="❌ Отмена", callback_data="cancel"),
|
||||
]
|
||||
)
|
||||
|
||||
]
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
def cancel_keyboard():
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отменить", callback_data="cancel")]
|
||||
"""Клавиатура с кнопкой отмены (для плейлистов)"""
|
||||
keyboard = [
|
||||
[InlineKeyboardButton(text="⛔ Отменить загрузку", callback_data="cancel")]
|
||||
]
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
def playlist_keyboard(confirm=False):
|
||||
"""Клавиатура для плейлистов YouTube"""
|
||||
if confirm:
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton(text="✅ Да, загрузить все", callback_data="playlist_confirm_yes"),
|
||||
InlineKeyboardButton(text="❌ Нет, отменить", callback_data="playlist_confirm_no"),
|
||||
]
|
||||
]
|
||||
)
|
||||
else:
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton(text="📁 Загрузить все видео", callback_data="playlist_all"),
|
||||
InlineKeyboardButton(text="🎬 Только первое видео", callback_data="playlist_first"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="❌ Отмена", callback_data="cancel"),
|
||||
]
|
||||
]
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
771
bot/main.py
771
bot/main.py
@@ -1,10 +1,10 @@
|
||||
# main.py
|
||||
import os
|
||||
import asyncio
|
||||
import threading
|
||||
import logging
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import Bot, Dispatcher, F
|
||||
from aiogram.types import Message, CallbackQuery, FSInputFile
|
||||
@@ -17,29 +17,25 @@ from config import (
|
||||
CACHE_DIR,
|
||||
TMP_DIR,
|
||||
COOKIES_FILE,
|
||||
RATE_LIMIT_SECONDS,
|
||||
CACHE_MAX_AGE_DAYS,
|
||||
CACHE_MAX_SIZE_MB,
|
||||
)
|
||||
from keyboards import quality_keyboard, cancel_keyboard
|
||||
from downloader import download_video, download_audio, DownloadCancelled
|
||||
from keyboards import youtube_quality_keyboard, cancel_keyboard, playlist_keyboard
|
||||
from downloader import (
|
||||
download_tiktok_video_and_audio,
|
||||
download_instagram_video,
|
||||
download_video,
|
||||
download_audio,
|
||||
download_playlist_videos,
|
||||
optimize_for_telegram,
|
||||
DownloadCancelled,
|
||||
)
|
||||
from middleware import PrivateMiddleware
|
||||
from rate_limit import check_rate_limit
|
||||
from info import extract_info
|
||||
from info import extract_info, is_playlist, get_platform_info
|
||||
from cache import cache_key, cache_path
|
||||
from cleanup import cleanup_tmp
|
||||
|
||||
# -------------------- init --------------------
|
||||
|
||||
# Настройка логирования
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Создаем директории
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
os.makedirs(TMP_DIR, exist_ok=True)
|
||||
|
||||
# Инициализация бота с локальным API (если используется)
|
||||
if LOCAL_API_URL:
|
||||
api_server = TelegramAPIServer.from_base(LOCAL_API_URL)
|
||||
session = AiohttpSession(api=api_server)
|
||||
@@ -48,135 +44,18 @@ else:
|
||||
bot = Bot(token=BOT_TOKEN)
|
||||
|
||||
dp = Dispatcher()
|
||||
|
||||
# Регистрация middleware
|
||||
private_middleware = PrivateMiddleware()
|
||||
dp.message.middleware(private_middleware)
|
||||
dp.callback_query.middleware(private_middleware)
|
||||
|
||||
USER_URLS: dict[int, str] = {}
|
||||
USER_DATA: dict[int, dict] = {}
|
||||
ACTIVE_DOWNLOADS: dict[int, dict] = {}
|
||||
|
||||
# -------------------- cache cleaning --------------------
|
||||
|
||||
def cleanup_old_cache():
|
||||
"""
|
||||
Очищает старые файлы из кэша
|
||||
"""
|
||||
try:
|
||||
current_time = time.time()
|
||||
deleted_count = 0
|
||||
deleted_size = 0
|
||||
|
||||
# Удаляем файлы старше CACHE_MAX_AGE_DAYS дней
|
||||
if CACHE_MAX_AGE_DAYS > 0:
|
||||
cutoff_time = current_time - (CACHE_MAX_AGE_DAYS * 24 * 3600)
|
||||
|
||||
for root, dirs, files in os.walk(CACHE_DIR):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
file_mtime = os.path.getmtime(file_path)
|
||||
if file_mtime < cutoff_time:
|
||||
file_size = os.path.getsize(file_path)
|
||||
os.remove(file_path)
|
||||
deleted_count += 1
|
||||
deleted_size += file_size
|
||||
logger.info(f"Deleted old cache file: {file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting file {file}: {e}")
|
||||
|
||||
# Если указан максимальный размер кэша, проверяем его
|
||||
if CACHE_MAX_SIZE_MB > 0:
|
||||
total_size_mb = get_cache_size_mb()
|
||||
if total_size_mb > CACHE_MAX_SIZE_MB:
|
||||
# Сортируем файлы по времени изменения (старые первыми)
|
||||
files_with_mtime = []
|
||||
for root, dirs, files in os.walk(CACHE_DIR):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
mtime = os.path.getmtime(file_path)
|
||||
size = os.path.getsize(file_path)
|
||||
files_with_mtime.append((file_path, mtime, size))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Сортируем по времени (старые первыми)
|
||||
files_with_mtime.sort(key=lambda x: x[1])
|
||||
|
||||
# Удаляем старые файлы пока не достигнем лимита
|
||||
target_size_mb = CACHE_MAX_SIZE_MB * 0.8 # Оставляем 80% от лимита
|
||||
|
||||
for file_path, mtime, size in files_with_mtime:
|
||||
if total_size_mb <= target_size_mb:
|
||||
break
|
||||
|
||||
try:
|
||||
os.remove(file_path)
|
||||
deleted_count += 1
|
||||
deleted_size += size
|
||||
total_size_mb -= size / (1024 * 1024)
|
||||
logger.info(f"Deleted cache file to free space: {os.path.basename(file_path)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting file {file_path}: {e}")
|
||||
|
||||
if deleted_count > 0:
|
||||
logger.info(f"Cache cleanup: deleted {deleted_count} files, freed {deleted_size / (1024*1024):.2f} MB")
|
||||
else:
|
||||
logger.info("Cache cleanup: no files to delete")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in cache cleanup: {e}")
|
||||
|
||||
|
||||
def get_cache_size_mb():
|
||||
"""Возвращает размер кэша в МБ"""
|
||||
total_size = 0
|
||||
for root, dirs, files in os.walk(CACHE_DIR):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
total_size += os.path.getsize(file_path)
|
||||
except:
|
||||
pass
|
||||
return total_size / (1024 * 1024)
|
||||
|
||||
|
||||
async def scheduled_cache_cleanup():
|
||||
"""Периодическая очистка кэша"""
|
||||
# Запускаем очистку сразу при старте
|
||||
logger.info("Running initial cache cleanup...")
|
||||
await asyncio.to_thread(cleanup_old_cache)
|
||||
|
||||
last_cleanup_day = datetime.now().day
|
||||
|
||||
while True:
|
||||
try:
|
||||
now = datetime.now()
|
||||
current_day = now.day
|
||||
|
||||
# Запускаем очистку если наступил новый день И сейчас между 3:00 и 3:59
|
||||
if current_day != last_cleanup_day and now.hour == 3:
|
||||
logger.info("Starting scheduled cache cleanup...")
|
||||
await asyncio.to_thread(cleanup_old_cache)
|
||||
last_cleanup_day = current_day
|
||||
|
||||
# Ждем 5 минут перед следующей проверкой
|
||||
await asyncio.sleep(300)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in scheduled cleanup: {e}")
|
||||
await asyncio.sleep(60)
|
||||
|
||||
|
||||
# -------------------- helpers --------------------
|
||||
|
||||
def render_bar(percent: float, size: int = 10) -> str:
|
||||
filled = int(size * percent / 100)
|
||||
return "█" * filled + "░" * (size - filled)
|
||||
|
||||
|
||||
def make_progress_cb(loop, message):
|
||||
last_percent = {"value": 0}
|
||||
last_update = {"time": 0}
|
||||
@@ -185,189 +64,196 @@ def make_progress_cb(loop, message):
|
||||
try:
|
||||
downloaded = d.get("downloaded_bytes", 0)
|
||||
total = d.get("total_bytes") or d.get("total_bytes_estimate") or 1
|
||||
|
||||
# Избегаем деления на ноль
|
||||
if total <= 0:
|
||||
return
|
||||
|
||||
percent = min(100, downloaded * 100 / total)
|
||||
|
||||
# Обновляем раз в ~2% и не чаще чем раз в 2 секунды
|
||||
current_time = time.time()
|
||||
if percent - last_percent["value"] < 2 and current_time - last_update["time"] < 2:
|
||||
return
|
||||
|
||||
last_percent["value"] = percent
|
||||
last_update["time"] = current_time
|
||||
|
||||
bar = render_bar(percent)
|
||||
eta = d.get("eta")
|
||||
|
||||
# Безопасное форматирование ETA
|
||||
if eta is None or eta == "?":
|
||||
eta_str = "?"
|
||||
else:
|
||||
try:
|
||||
eta_str = str(int(float(eta)))
|
||||
except (ValueError, TypeError):
|
||||
eta_str = "?"
|
||||
|
||||
text = (
|
||||
"⏬ <b>Загрузка</b>\n"
|
||||
f"<code>{bar}</code> {percent:.0f}%\n"
|
||||
f"⏱ Осталось: {eta_str} сек"
|
||||
)
|
||||
|
||||
await message.edit_text(
|
||||
text,
|
||||
reply_markup=cancel_keyboard(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
eta_str = str(int(float(eta))) if eta and eta != "?" else "?"
|
||||
bar = render_bar(percent)
|
||||
text = f"⏬ <b>Загрузка</b>\n<code>{bar}</code> {percent:.0f}%\n⏱ Осталось: {eta_str} сек"
|
||||
await message.edit_text(text, reply_markup=cancel_keyboard() if "playlist" in message.text.lower() else None, parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating progress: {e}")
|
||||
logger.error(f"Progress error: {e}")
|
||||
|
||||
def cb(d):
|
||||
asyncio.run_coroutine_threadsafe(update(d), loop)
|
||||
|
||||
return cb
|
||||
|
||||
# ---------------- TikTok + Instagram (автозагрузка) ----------------
|
||||
async def process_tiktok_auto(message: Message, user_id: int, url: str):
|
||||
status = await message.answer("🎵 <b>Загружаю TikTok (видео + аудио)...</b>", parse_mode="HTML")
|
||||
|
||||
key_video = cache_key(url, "tiktok_video", audio=False)
|
||||
key_audio = cache_key(url, "tiktok_audio", audio=True)
|
||||
|
||||
video_cache = cache_path(CACHE_DIR, key_video, "mp4")
|
||||
tmp_video = cache_path(TMP_DIR, key_video, "mp4")
|
||||
os.makedirs(os.path.dirname(tmp_video), exist_ok=True)
|
||||
|
||||
audio_cache = cache_path(CACHE_DIR, key_audio, "mp3")
|
||||
tmp_audio = cache_path(TMP_DIR, key_audio, "mp3")
|
||||
os.makedirs(os.path.dirname(tmp_audio), exist_ok=True)
|
||||
|
||||
if os.path.exists(video_cache) and os.path.exists(audio_cache):
|
||||
await status.edit_text("📤 <b>Отправляю из кэша...</b>", parse_mode="HTML")
|
||||
try:
|
||||
await message.answer_video(FSInputFile(video_cache), supports_streaming=True)
|
||||
await message.answer_audio(FSInputFile(audio_cache))
|
||||
size_mb = (os.path.getsize(video_cache) + os.path.getsize(audio_cache)) / (1024 * 1024)
|
||||
await message.answer(f"✅ <b>Готово! (TikTok)</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logger.error(f"TikTok cache error: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке")
|
||||
finally:
|
||||
cleanup_tmp(TMP_DIR)
|
||||
return
|
||||
|
||||
cancel_event = threading.Event()
|
||||
ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event}
|
||||
loop = asyncio.get_running_loop()
|
||||
progress_cb = make_progress_cb(loop, status)
|
||||
|
||||
def optimize_for_telegram(input_path: str, output_path: str) -> bool:
|
||||
"""
|
||||
Оптимизирует видео для телеграма
|
||||
"""
|
||||
try:
|
||||
# Проверяем размер файла
|
||||
file_size_mb = os.path.getsize(input_path) / (1024 * 1024)
|
||||
|
||||
# Если файл больше 50 МБ, сжимаем его
|
||||
if file_size_mb > 50:
|
||||
crf = 28
|
||||
else:
|
||||
crf = 23
|
||||
|
||||
cmd = [
|
||||
'ffmpeg',
|
||||
'-i', input_path,
|
||||
'-c:v', 'libx264',
|
||||
'-preset', 'fast',
|
||||
'-crf', str(crf),
|
||||
'-c:a', 'aac',
|
||||
'-b:a', '128k',
|
||||
'-movflags', '+faststart',
|
||||
'-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
|
||||
'-y',
|
||||
output_path
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300
|
||||
await asyncio.to_thread(
|
||||
download_tiktok_video_and_audio,
|
||||
url,
|
||||
tmp_video,
|
||||
tmp_audio,
|
||||
COOKIES_FILE,
|
||||
cancel_event,
|
||||
progress_cb,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"FFmpeg error: {result.stderr}")
|
||||
import shutil
|
||||
shutil.copy2(input_path, output_path)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if cancel_event.is_set():
|
||||
await status.edit_text("⛔ Загрузка отменена")
|
||||
for p in [tmp_video, tmp_audio]:
|
||||
if os.path.exists(p): os.remove(p)
|
||||
return
|
||||
|
||||
# Уже готовы Telegram-safe mp4/m3, просто перемещаем
|
||||
if os.path.exists(video_cache): os.remove(video_cache)
|
||||
os.rename(tmp_video, video_cache)
|
||||
if os.path.exists(audio_cache): os.remove(audio_cache)
|
||||
os.rename(tmp_audio, audio_cache)
|
||||
|
||||
await status.edit_text("📤 <b>Отправляю видео + аудио...</b>", parse_mode="HTML")
|
||||
await message.answer_video(FSInputFile(video_cache), supports_streaming=True)
|
||||
await message.answer_audio(FSInputFile(audio_cache))
|
||||
size_mb = (os.path.getsize(video_cache) + os.path.getsize(audio_cache)) / (1024 * 1024)
|
||||
await message.answer(f"✅ <b>Готово! (TikTok)</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing video: {e}")
|
||||
import shutil
|
||||
shutil.copy2(input_path, output_path)
|
||||
return False
|
||||
logger.error(f"TikTok download error: {e}")
|
||||
await status.edit_text(f"❌ Ошибка: {str(e)[:120]}")
|
||||
finally:
|
||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
async def process_instagram_auto(message: Message, user_id: int, url: str):
|
||||
status = await message.answer("📸 <b>Загружаю Instagram (1080p)...</b>", parse_mode="HTML")
|
||||
|
||||
# -------------------- handlers --------------------
|
||||
key = cache_key(url, "instagram_video", audio=False)
|
||||
final_cache = cache_path(CACHE_DIR, key, "mp4")
|
||||
tmp_path = cache_path(TMP_DIR, key, "mp4")
|
||||
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
||||
|
||||
if os.path.exists(final_cache):
|
||||
await status.edit_text("📤 <b>Отправляю из кэша...</b>", parse_mode="HTML")
|
||||
try:
|
||||
await message.answer_video(FSInputFile(final_cache), supports_streaming=True)
|
||||
size_mb = os.path.getsize(final_cache) / (1024 * 1024)
|
||||
await message.answer(f"✅ <b>Готово! (Instagram 1080p)</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logger.error(f"Instagram cache error: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке")
|
||||
finally:
|
||||
cleanup_tmp(TMP_DIR)
|
||||
return
|
||||
|
||||
cancel_event = threading.Event()
|
||||
ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event}
|
||||
loop = asyncio.get_running_loop()
|
||||
progress_cb = make_progress_cb(loop, status)
|
||||
|
||||
try:
|
||||
await asyncio.to_thread(
|
||||
download_instagram_video,
|
||||
url,
|
||||
tmp_path,
|
||||
COOKIES_FILE,
|
||||
cancel_event,
|
||||
progress_cb,
|
||||
)
|
||||
if cancel_event.is_set():
|
||||
await status.edit_text("⛔ Загрузка отменена")
|
||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||
return
|
||||
|
||||
if os.path.exists(final_cache): os.remove(final_cache)
|
||||
os.rename(tmp_path, final_cache)
|
||||
|
||||
await status.edit_text("📤 <b>Отправляю видео...</b>", parse_mode="HTML")
|
||||
await message.answer_video(FSInputFile(final_cache), supports_streaming=True)
|
||||
size_mb = os.path.getsize(final_cache) / (1024 * 1024)
|
||||
await message.answer(f"✅ <b>Готово! (Instagram 1080p)</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Instagram download error: {e}")
|
||||
await status.edit_text(f"❌ Ошибка: {str(e)[:120]}")
|
||||
finally:
|
||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
# ---------------- Handlers ----------------
|
||||
@dp.message(F.text == "/start")
|
||||
async def start(message: Message):
|
||||
await message.answer(
|
||||
"👋 <b>Привет!</b>\n\n"
|
||||
"📥 Я скачиваю <b>видео</b> и <b>звук из видео</b> по ссылке.\n\n"
|
||||
"🔄 Кэш автоматически очищается раз в сутки\n"
|
||||
"📥 Скачиваю видео и аудио по ссылке.\n\n"
|
||||
"✨ <b>Новые возможности:</b>\n"
|
||||
"• 🎬 TikTok: автоматически видео + аудио (Telegram-safe)\n"
|
||||
"• 📸 Instagram: автоматически видео в 1080p (Telegram-safe)\n"
|
||||
"• 🎵 Аудио из любого видео\n"
|
||||
"• 📁 Плейлисты YouTube\n"
|
||||
"👉 Просто отправь ссылку.",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
@dp.message(F.text == "/cache_stats")
|
||||
async def cache_stats(message: Message):
|
||||
"""Показывает статистику кэша"""
|
||||
try:
|
||||
total_size_mb = get_cache_size_mb()
|
||||
file_count = 0
|
||||
|
||||
for root, dirs, files in os.walk(CACHE_DIR):
|
||||
file_count += len(files)
|
||||
|
||||
# Получаем время последнего изменения самого старого файла
|
||||
oldest_time = None
|
||||
newest_time = None
|
||||
|
||||
for root, dirs, files in os.walk(CACHE_DIR):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
mtime = os.path.getmtime(file_path)
|
||||
if oldest_time is None or mtime < oldest_time:
|
||||
oldest_time = mtime
|
||||
if newest_time is None or mtime > newest_time:
|
||||
newest_time = mtime
|
||||
except:
|
||||
pass
|
||||
|
||||
if oldest_time:
|
||||
oldest_str = datetime.fromtimestamp(oldest_time).strftime("%d.%m.%Y %H:%M")
|
||||
newest_str = datetime.fromtimestamp(newest_time).strftime("%d.%m.%Y %H:%M")
|
||||
age_info = f"🗓 Самый старый: {oldest_str}\n" \
|
||||
f"🆕 Самый новый: {newest_str}"
|
||||
else:
|
||||
age_info = "🗓 Кэш пуст"
|
||||
|
||||
await message.answer(
|
||||
f"📊 <b>Статистика кэша:</b>\n\n"
|
||||
f"📁 Файлов: {file_count}\n"
|
||||
f"💾 Размер: {total_size_mb:.2f} МБ\n"
|
||||
f"⏰ Очистка: ежедневно в 3:00\n\n"
|
||||
f"{age_info}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting cache stats: {e}")
|
||||
await message.answer("❌ Ошибка при получении статистики кэша")
|
||||
|
||||
|
||||
@dp.message(F.text.startswith("http"))
|
||||
async def handle_link(message: Message):
|
||||
url = message.text.strip()
|
||||
USER_URLS[message.from_user.id] = url
|
||||
user_id = message.from_user.id
|
||||
USER_URLS[user_id] = url
|
||||
|
||||
platform_info = await asyncio.to_thread(get_platform_info, url)
|
||||
|
||||
if platform_info == "tiktok":
|
||||
await process_tiktok_auto(message, user_id, url)
|
||||
return
|
||||
|
||||
if platform_info == "instagram":
|
||||
await process_instagram_auto(message, user_id, url)
|
||||
return
|
||||
|
||||
if await asyncio.to_thread(is_playlist, url):
|
||||
await message.answer(
|
||||
"📁 <b>Обнаружен плейлист!</b>\n\n"
|
||||
"Выберите действие:",
|
||||
reply_markup=playlist_keyboard(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
await message.answer(
|
||||
"🔽 <b>Выбери формат загрузки:</b>",
|
||||
reply_markup=quality_keyboard(),
|
||||
"🔽 <b>Выбери качество:</b>",
|
||||
reply_markup=youtube_quality_keyboard(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
@dp.callback_query(F.data == "cancel")
|
||||
async def cancel_download(callback: CallbackQuery):
|
||||
user_id = callback.from_user.id
|
||||
data = ACTIVE_DOWNLOADS.get(user_id)
|
||||
|
||||
if data:
|
||||
data["cancel"].set()
|
||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||
await callback.answer("⛔ Загрузка отменена", show_alert=True)
|
||||
else:
|
||||
await callback.answer("❌ Нет активной загрузки", show_alert=True)
|
||||
|
||||
|
||||
# ---------------- VIDEO ----------------
|
||||
|
||||
@dp.callback_query(F.data.startswith("q:"))
|
||||
async def handle_video(callback: CallbackQuery):
|
||||
await callback.answer()
|
||||
@@ -379,39 +265,20 @@ async def handle_video(callback: CallbackQuery):
|
||||
await callback.message.answer("❌ Ссылка не найдена")
|
||||
return
|
||||
|
||||
if not check_rate_limit(user_id, RATE_LIMIT_SECONDS):
|
||||
await callback.message.answer("⏳ Подожди немного перед следующим запросом")
|
||||
return
|
||||
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
status = await callback.message.answer("🔍 <b>Анализирую ссылку…</b>", parse_mode="HTML")
|
||||
|
||||
# Извлекаем информацию о видео
|
||||
try:
|
||||
info = await asyncio.to_thread(extract_info, url, COOKIES_FILE)
|
||||
if not info:
|
||||
await status.edit_text("❌ Не удалось получить информацию о видео")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting info: {e}")
|
||||
await status.edit_text("❌ Ошибка при анализе ссылки")
|
||||
return
|
||||
|
||||
key = cache_key(url, quality, audio=False)
|
||||
final_path = cache_path(CACHE_DIR, key, "mp4")
|
||||
tmp_path = os.path.join(TMP_DIR, f"{key}.mp4")
|
||||
optimized_path = os.path.join(TMP_DIR, f"{key}_optimized.mp4")
|
||||
tmp_path = cache_path(TMP_DIR, key, "mp4")
|
||||
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
||||
|
||||
# Проверяем кэш
|
||||
if os.path.exists(final_path):
|
||||
await status.edit_text("📤 <b>Отправляю файл из кэша…</b>", parse_mode="HTML")
|
||||
try:
|
||||
await callback.message.answer_video(FSInputFile(final_path))
|
||||
size_mb = os.path.getsize(final_path) / 1024 / 1024
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
size_mb = os.path.getsize(final_path) / (1024 * 1024)
|
||||
await callback.message.answer(f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending cached file: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке файла")
|
||||
@@ -432,74 +299,39 @@ async def handle_video(callback: CallbackQuery):
|
||||
cancel_event,
|
||||
progress_cb,
|
||||
)
|
||||
|
||||
# Проверяем, был ли отменен процесс
|
||||
if cancel_event.is_set():
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||
await status.edit_text("⛔ Загрузка отменена")
|
||||
return
|
||||
|
||||
# Оптимизируем видео для телеграма
|
||||
await status.edit_text("⚙️ <b>Оптимизирую видео для телеграма…</b>", parse_mode="HTML")
|
||||
await asyncio.to_thread(optimize_for_telegram, tmp_path, optimized_path)
|
||||
|
||||
# Удаляем исходный файл и используем оптимизированный
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
|
||||
os.rename(optimized_path, final_path)
|
||||
|
||||
|
||||
if os.path.exists(final_path): os.remove(final_path)
|
||||
os.rename(tmp_path, final_path)
|
||||
|
||||
except DownloadCancelled:
|
||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||
await status.edit_text("⛔ Загрузка отменена")
|
||||
for path in [tmp_path, optimized_path]:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading video: {e}")
|
||||
await status.edit_text(
|
||||
"❌ Не удалось скачать видео\n"
|
||||
"💡 Попробуй другое качество"
|
||||
)
|
||||
for path in [tmp_path, optimized_path]:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||
await status.edit_text("❌ Не удалось скачать видео")
|
||||
return
|
||||
finally:
|
||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||
|
||||
await status.edit_text("📤 <b>Отправляю видео…</b>", parse_mode="HTML")
|
||||
|
||||
try:
|
||||
await callback.message.answer_video(
|
||||
FSInputFile(final_path),
|
||||
supports_streaming=True
|
||||
)
|
||||
size_mb = os.path.getsize(final_path) / 1024 / 1024
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.message.answer_video(FSInputFile(final_path), supports_streaming=True)
|
||||
size_mb = os.path.getsize(final_path) / (1024 * 1024)
|
||||
await callback.message.answer(f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending video: {e}")
|
||||
try:
|
||||
await callback.message.answer_document(FSInputFile(final_path))
|
||||
size_mb = os.path.getsize(final_path) / 1024 / 1024
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Отправлено как документ</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
size_mb = os.path.getsize(final_path) / (1024 * 1024)
|
||||
await callback.message.answer(f"✅ <b>Отправлено как документ</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
except Exception as e2:
|
||||
logger.error(f"Error sending as document: {e2}")
|
||||
await status.edit_text("❌ Ошибка при отправке файла")
|
||||
if os.path.exists(final_path):
|
||||
os.remove(final_path)
|
||||
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
|
||||
# ---------------- AUDIO FROM VIDEO ----------------
|
||||
|
||||
@dp.callback_query(F.data == "audio")
|
||||
async def handle_audio(callback: CallbackQuery):
|
||||
@@ -511,31 +343,20 @@ async def handle_audio(callback: CallbackQuery):
|
||||
await callback.message.answer("❌ Ссылка не найдена")
|
||||
return
|
||||
|
||||
if not check_rate_limit(user_id, RATE_LIMIT_SECONDS):
|
||||
await callback.message.answer("⏳ Подожди немного перед следующим запросом")
|
||||
return
|
||||
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
status = await callback.message.answer("🎧 <b>Подготовка аудио…</b>", parse_mode="HTML")
|
||||
|
||||
key = cache_key(url, "audio", audio=True)
|
||||
final_path = cache_path(CACHE_DIR, key, "mp3")
|
||||
tmp_path = os.path.join(TMP_DIR, f"{key}.mp3")
|
||||
tmp_path = cache_path(TMP_DIR, key, "mp3")
|
||||
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
|
||||
|
||||
# Создаем директории если не существуют
|
||||
os.makedirs(TMP_DIR, exist_ok=True)
|
||||
os.makedirs(os.path.dirname(final_path), exist_ok=True)
|
||||
|
||||
# Проверяем кэш
|
||||
if os.path.exists(final_path):
|
||||
await status.edit_text("📤 <b>Отправляю аудио из кэша…</b>", parse_mode="HTML")
|
||||
try:
|
||||
await callback.message.answer_audio(FSInputFile(final_path))
|
||||
size_mb = os.path.getsize(final_path) / 1024 / 1024
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
size_mb = os.path.getsize(final_path) / (1024 * 1024)
|
||||
await callback.message.answer(f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending cached audio: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке аудио")
|
||||
@@ -555,85 +376,213 @@ async def handle_audio(callback: CallbackQuery):
|
||||
cancel_event,
|
||||
progress_cb,
|
||||
)
|
||||
|
||||
# Проверяем, был ли отменен процесс
|
||||
if cancel_event.is_set():
|
||||
await status.edit_text("⛔ Загрузка отменена")
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||
return
|
||||
|
||||
# Проверяем, создан ли файл
|
||||
|
||||
if not os.path.exists(tmp_path):
|
||||
raise Exception("Аудио файл не был создан")
|
||||
|
||||
# Проверяем размер файла
|
||||
file_size = os.path.getsize(tmp_path)
|
||||
if file_size == 0:
|
||||
if os.path.getsize(tmp_path) == 0:
|
||||
os.remove(tmp_path)
|
||||
raise Exception("Создан пустой аудио файл")
|
||||
|
||||
# Перемещаем файл в кэш
|
||||
if os.path.exists(final_path):
|
||||
os.remove(final_path)
|
||||
|
||||
if os.path.exists(final_path): os.remove(final_path)
|
||||
os.rename(tmp_path, final_path)
|
||||
|
||||
|
||||
except DownloadCancelled:
|
||||
await status.edit_text("⛔ Загрузка отменена")
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading audio: {str(e)}")
|
||||
await status.edit_text(f"❌ Ошибка: {str(e)[:100]}")
|
||||
if os.path.exists(tmp_path):
|
||||
try:
|
||||
os.remove(tmp_path)
|
||||
except:
|
||||
pass
|
||||
if os.path.exists(tmp_path): os.remove(tmp_path)
|
||||
return
|
||||
finally:
|
||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||
|
||||
await status.edit_text("📤 <b>Отправляю аудио…</b>", parse_mode="HTML")
|
||||
|
||||
try:
|
||||
await callback.message.answer_audio(FSInputFile(final_path))
|
||||
size_mb = os.path.getsize(final_path) / 1024 / 1024
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
size_mb = os.path.getsize(final_path) / (1024 * 1024)
|
||||
await callback.message.answer(f"✅ <b>Готово!</b>\n📦 Размер: {size_mb:.1f} МБ", parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending audio: {e}")
|
||||
await status.edit_text("❌ Ошибка при отправке аудио")
|
||||
|
||||
cleanup_tmp(TMP_DIR)
|
||||
# ---------------- Playlist Handlers ----------------
|
||||
@dp.callback_query(F.data == "playlist_all")
|
||||
async def handle_playlist_all(callback: CallbackQuery):
|
||||
await callback.answer()
|
||||
user_id = callback.from_user.id
|
||||
url = USER_URLS.get(user_id)
|
||||
if not url:
|
||||
await callback.message.answer("❌ Ссылка не найдена")
|
||||
return
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
status = await callback.message.answer("📁 <b>Анализирую плейлист…</b>", parse_mode="HTML")
|
||||
try:
|
||||
from info import get_playlist_info
|
||||
playlist_info = await asyncio.to_thread(get_playlist_info, url)
|
||||
if not playlist_info or 'entries' not in playlist_info:
|
||||
await status.edit_text("❌ Не удалось получить информацию о плейлисте")
|
||||
return
|
||||
video_count = len(playlist_info['entries'])
|
||||
if video_count == 0:
|
||||
await status.edit_text("❌ Плейлист пуст")
|
||||
return
|
||||
if video_count > 10:
|
||||
await callback.message.answer(
|
||||
f"⚠️ <b>Внимание!</b>\n\n"
|
||||
f"Плейлист содержит <b>{video_count}</b> видео.\n"
|
||||
f"Это может занять много времени и места.\n\n"
|
||||
f"Продолжить загрузку?",
|
||||
reply_markup=playlist_keyboard(confirm=True),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
USER_DATA[user_id] = {"playlist_info": playlist_info, "status_message": status}
|
||||
return
|
||||
await download_playlist_confirm(callback, user_id, playlist_info, status)
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing playlist: {e}")
|
||||
await status.edit_text("❌ Ошибка при анализе плейлиста")
|
||||
|
||||
@dp.callback_query(F.data == "playlist_confirm_yes")
|
||||
async def handle_playlist_confirm(callback: CallbackQuery):
|
||||
await callback.answer()
|
||||
user_id = callback.from_user.id
|
||||
data = USER_DATA.get(user_id, {})
|
||||
playlist_info = data.get("playlist_info")
|
||||
status = data.get("status_message")
|
||||
if not playlist_info or not status:
|
||||
await callback.message.answer("❌ Данные плейлиста не найдены")
|
||||
return
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
await download_playlist_confirm(callback, user_id, playlist_info, status)
|
||||
|
||||
# -------------------- entrypoint --------------------
|
||||
async def download_playlist_confirm(callback, user_id, playlist_info, status):
|
||||
import uuid
|
||||
import shutil
|
||||
video_count = len(playlist_info['entries'])
|
||||
playlist_title = playlist_info.get('title', 'Плейлист')
|
||||
await status.edit_text(
|
||||
f"📁 <b>Начинаю загрузку плейлиста</b>\n\n"
|
||||
f"🎬 Название: {playlist_title}\n"
|
||||
f"📹 Видео: {video_count}\n"
|
||||
f"⏳ Подготовка...",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
cancel_event = threading.Event()
|
||||
ACTIVE_DOWNLOADS[user_id] = {"cancel": cancel_event}
|
||||
loop = asyncio.get_running_loop()
|
||||
progress_cb = make_progress_cb(loop, status)
|
||||
playlist_dir = os.path.join(TMP_DIR, f"playlist_{uuid.uuid4().hex[:8]}")
|
||||
os.makedirs(playlist_dir, exist_ok=True)
|
||||
try:
|
||||
downloaded_files = await asyncio.to_thread(
|
||||
download_playlist_videos,
|
||||
playlist_info,
|
||||
playlist_dir,
|
||||
COOKIES_FILE,
|
||||
cancel_event,
|
||||
progress_cb
|
||||
)
|
||||
if cancel_event.is_set():
|
||||
await status.edit_text("⛔ Загрузка плейлиста отменена")
|
||||
shutil.rmtree(playlist_dir, ignore_errors=True)
|
||||
return
|
||||
if not downloaded_files:
|
||||
await status.edit_text("❌ Не удалось загрузить видео из плейлиста")
|
||||
shutil.rmtree(playlist_dir, ignore_errors=True)
|
||||
return
|
||||
await status.edit_text(f"📤 <b>Отправляю {len(downloaded_files)} видео…</b>", parse_mode="HTML")
|
||||
downloaded_files.sort(key=lambda x: os.path.getsize(x))
|
||||
sent_count = 0
|
||||
for i, file_path in enumerate(downloaded_files, 1):
|
||||
if cancel_event.is_set():
|
||||
break
|
||||
try:
|
||||
file_name = os.path.basename(file_path)
|
||||
display_name = os.path.splitext(file_name)[0]
|
||||
await callback.message.answer_document(
|
||||
FSInputFile(file_path),
|
||||
caption=f"🎬 Видео {i}/{len(downloaded_files)}\n📁 {display_name[:50]}"
|
||||
)
|
||||
sent_count += 1
|
||||
await asyncio.sleep(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending file {file_path}: {e}")
|
||||
continue
|
||||
total_size = sum(os.path.getsize(f) for f in downloaded_files)
|
||||
total_size_mb = total_size / (1024 * 1024)
|
||||
await callback.message.answer(
|
||||
f"✅ <b>Плейлист загружен!</b>\n\n"
|
||||
f"📁 Видео в плейлисте: {video_count}\n"
|
||||
f"📤 Отправлено: {sent_count}\n"
|
||||
f"💾 Общий размер: {total_size_mb:.1f} МБ\n"
|
||||
f"🎬 Название: {playlist_title}",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
shutil.rmtree(playlist_dir, ignore_errors=True)
|
||||
except DownloadCancelled:
|
||||
await status.edit_text("⛔ Загрузка плейлиста отменена")
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading playlist: {e}")
|
||||
await status.edit_text(f"❌ Ошибка: {str(e)[:100]}")
|
||||
finally:
|
||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
@dp.callback_query(F.data == "playlist_confirm_no")
|
||||
async def handle_playlist_cancel(callback: CallbackQuery):
|
||||
await callback.answer("Отменено", show_alert=True)
|
||||
|
||||
@dp.callback_query(F.data == "playlist_first")
|
||||
async def handle_playlist_first(callback: CallbackQuery):
|
||||
await callback.answer()
|
||||
user_id = callback.from_user.id
|
||||
url = USER_URLS.get(user_id)
|
||||
if not url:
|
||||
await callback.message.answer("❌ Ссылка не найдена")
|
||||
return
|
||||
try:
|
||||
from info import get_first_video_from_playlist
|
||||
video_url = await asyncio.to_thread(get_first_video_from_playlist, url)
|
||||
if not video_url:
|
||||
await callback.message.answer("❌ Не удалось получить видео из плейлиста")
|
||||
return
|
||||
USER_URLS[user_id] = video_url
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
await callback.message.answer(
|
||||
"🔽 <b>Выбери формат загрузки для первого видео:</b>",
|
||||
reply_markup=youtube_quality_keyboard(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting first video: {e}")
|
||||
await callback.message.answer("❌ Ошибка при получении видео из плейлиста")
|
||||
|
||||
@dp.callback_query(F.data == "cancel")
|
||||
async def cancel_download(callback: CallbackQuery):
|
||||
user_id = callback.from_user.id
|
||||
data = ACTIVE_DOWNLOADS.get(user_id)
|
||||
if data:
|
||||
data["cancel"].set()
|
||||
ACTIVE_DOWNLOADS.pop(user_id, None)
|
||||
await callback.answer("⛔ Загрузка отменена", show_alert=True)
|
||||
else:
|
||||
await callback.answer("❌ Нет активной загрузки", show_alert=True)
|
||||
|
||||
async def main():
|
||||
# Очистка временных файлов при старте
|
||||
cleanup_tmp(TMP_DIR)
|
||||
|
||||
# Запускаем задачу очистки кэша в фоне
|
||||
cleanup_task = asyncio.create_task(scheduled_cache_cleanup())
|
||||
|
||||
try:
|
||||
await dp.start_polling(bot)
|
||||
finally:
|
||||
# Отменяем задачу очистки при выходе
|
||||
cleanup_task.cancel()
|
||||
try:
|
||||
await cleanup_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Bot stopped by user")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Bot stopped by user")
|
||||
except Exception as e:
|
||||
logger.error(f"Fatal error: {e}")
|
||||
BIN
wireguard/.DS_Store
vendored
Normal file
BIN
wireguard/.DS_Store
vendored
Normal file
Binary file not shown.
11
wireguard/wg_confs/wg0.conf
Normal file
11
wireguard/wg_confs/wg0.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
[Interface]
|
||||
Address = 10.7.0.13/24
|
||||
DNS = 1.0.0.1, 1.1.1.1, 1.0.0.1, 1.1.1.1
|
||||
PrivateKey = 8GTpMYtpOhWDp6AKXxukwFRsiwvERf2UVCjCrdB+M10=
|
||||
|
||||
[Peer]
|
||||
PublicKey = ekvrE4SMgydQhkWdWgJazlrILSEAlRbwCI4KcXUQv3A=
|
||||
PresharedKey = eaCOqeiYdfso4v7/ucrvFzB1MtDnXy1yTiujL08jJIE=
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
Endpoint = 109.107.170.183:51821
|
||||
PersistentKeepalive = 25
|
||||
Reference in New Issue
Block a user