|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__author__ = "Benny <[email protected]>" |
|
|
|
import logging |
|
import os |
|
import pathlib |
|
import random |
|
import re |
|
import subprocess |
|
import time |
|
import traceback |
|
from io import StringIO |
|
from unittest.mock import MagicMock |
|
|
|
import fakeredis |
|
import ffmpeg |
|
import ffpb |
|
import filetype |
|
import requests |
|
import yt_dlp as ytdl |
|
from pyrogram import types |
|
from tqdm import tqdm |
|
|
|
from config import AUDIO_FORMAT, ENABLE_ARIA2, ENABLE_FFMPEG, TG_MAX_SIZE, IPv6 |
|
from limit import Payment |
|
from utils import adjust_formats, apply_log_formatter, current_time, sizeof_fmt |
|
|
|
r = fakeredis.FakeStrictRedis() |
|
apply_log_formatter() |
|
|
|
|
|
def edit_text(bot_msg: types.Message, text: str): |
|
key = f"{bot_msg.chat.id}-{bot_msg.id}" |
|
|
|
if not r.exists(key): |
|
time.sleep(random.random()) |
|
r.set(key, "ok", ex=3) |
|
bot_msg.edit_text(text) |
|
|
|
|
|
def tqdm_progress(desc, total, finished, speed="", eta=""): |
|
def more(title, initial): |
|
if initial: |
|
return f"{title} {initial}" |
|
else: |
|
return "" |
|
|
|
f = StringIO() |
|
tqdm( |
|
total=total, |
|
initial=finished, |
|
file=f, |
|
ascii=False, |
|
unit_scale=True, |
|
ncols=30, |
|
bar_format="{l_bar}{bar} |{n_fmt}/{total_fmt} ", |
|
) |
|
raw_output = f.getvalue() |
|
tqdm_output = raw_output.split("|") |
|
progress = f"`[{tqdm_output[1]}]`" |
|
detail = tqdm_output[2].replace("[A", "") |
|
text = f""" |
|
{desc} |
|
|
|
{progress} |
|
{detail} |
|
{more("Speed:", speed)} |
|
{more("ETA:", eta)} |
|
""" |
|
f.close() |
|
return text |
|
|
|
|
|
def remove_bash_color(text): |
|
return re.sub(r"\u001b|\[0;94m|\u001b\[0m|\[0;32m|\[0m|\[0;33m", "", text) |
|
|
|
|
|
def download_hook(d: dict, bot_msg): |
|
|
|
|
|
|
|
original_url = d["info_dict"]["original_url"] |
|
key = f"{bot_msg.chat.id}-{original_url}" |
|
|
|
if d["status"] == "downloading": |
|
downloaded = d.get("downloaded_bytes", 0) |
|
total = d.get("total_bytes") or d.get("total_bytes_estimate", 0) |
|
if total > TG_MAX_SIZE: |
|
raise Exception(f"Your download file size {sizeof_fmt(total)} is too large for Telegram.") |
|
|
|
|
|
speed = remove_bash_color(d.get("_speed_str", "N/A")) |
|
eta = remove_bash_color(d.get("_eta_str", d.get("eta"))) |
|
text = tqdm_progress("Downloading...", total, downloaded, speed, eta) |
|
edit_text(bot_msg, text) |
|
r.set(key, "ok", ex=5) |
|
|
|
|
|
def upload_hook(current, total, bot_msg): |
|
text = tqdm_progress("Uploading...", total, current) |
|
edit_text(bot_msg, text) |
|
|
|
|
|
def convert_to_mp4(video_paths: list, bot_msg): |
|
default_type = ["video/x-flv", "video/webm"] |
|
|
|
for path in video_paths: |
|
|
|
mime = getattr(filetype.guess(path), "mime", "video/mp4") |
|
if mime in default_type: |
|
if not can_convert_mp4(path, bot_msg.chat.id): |
|
logging.warning("Conversion abort for %s", bot_msg.chat.id) |
|
bot_msg._client.send_message(bot_msg.chat.id, "Can't convert your video. ffmpeg has been disabled.") |
|
break |
|
edit_text(bot_msg, f"{current_time()}: Converting {path.name} to mp4. Please wait.") |
|
new_file_path = path.with_suffix(".mp4") |
|
logging.info("Detected %s, converting to mp4...", mime) |
|
run_ffmpeg_progressbar(["ffmpeg", "-y", "-i", path, new_file_path], bot_msg) |
|
index = video_paths.index(path) |
|
video_paths[index] = new_file_path |
|
|
|
|
|
class ProgressBar(tqdm): |
|
b = None |
|
|
|
def __init__(self, *args, **kwargs): |
|
super().__init__(*args, **kwargs) |
|
self.bot_msg = self.b |
|
|
|
def update(self, n=1): |
|
super().update(n) |
|
t = tqdm_progress("Converting...", self.total, self.n) |
|
edit_text(self.bot_msg, t) |
|
|
|
|
|
def run_ffmpeg_progressbar(cmd_list: list, bm): |
|
cmd_list = cmd_list.copy()[1:] |
|
ProgressBar.b = bm |
|
ffpb.main(cmd_list, tqdm=ProgressBar) |
|
|
|
|
|
def can_convert_mp4(video_path, uid): |
|
if not ENABLE_FFMPEG: |
|
return False |
|
return True |
|
|
|
|
|
def ytdl_download(url: str, tempdir: str, bm, **kwargs) -> list: |
|
payment = Payment() |
|
chat_id = bm.chat.id |
|
hijack = kwargs.get("hijack") |
|
output = pathlib.Path(tempdir, "%(title).70s.%(ext)s").as_posix() |
|
ydl_opts = { |
|
"progress_hooks": [lambda d: download_hook(d, bm)], |
|
"outtmpl": output, |
|
"restrictfilenames": False, |
|
"quiet": True, |
|
} |
|
if ENABLE_ARIA2: |
|
ydl_opts["external_downloader"] = "aria2c" |
|
ydl_opts["external_downloader_args"] = [ |
|
"--min-split-size=1M", |
|
"--max-connection-per-server=16", |
|
"--max-concurrent-downloads=16", |
|
"--split=16", |
|
] |
|
if url.startswith("https://drive.google.com"): |
|
|
|
formats = ["source"] |
|
else: |
|
|
|
formats = [ |
|
|
|
"bestvideo[ext=mp4][vcodec!*=av01][vcodec!*=vp09]+bestaudio[ext=m4a]/bestvideo+bestaudio", |
|
"bestvideo[vcodec^=avc]+bestaudio[acodec^=mp4a]/best[vcodec^=avc]/best", |
|
None, |
|
] |
|
adjust_formats(chat_id, url, formats, hijack) |
|
if download_instagram(url, tempdir): |
|
return list(pathlib.Path(tempdir).glob("*")) |
|
|
|
address = ["::", "0.0.0.0"] if IPv6 else [None] |
|
error = None |
|
video_paths = None |
|
for format_ in formats: |
|
ydl_opts["format"] = format_ |
|
for addr in address: |
|
|
|
ydl_opts["source_address"] = addr |
|
try: |
|
logging.info("Downloading for %s with format %s", url, format_) |
|
with ytdl.YoutubeDL(ydl_opts) as ydl: |
|
ydl.download([url]) |
|
video_paths = list(pathlib.Path(tempdir).glob("*")) |
|
break |
|
except Exception: |
|
error = traceback.format_exc() |
|
logging.error("Download failed for %s - %s, try another way", format_, url) |
|
if error is None: |
|
break |
|
|
|
if not video_paths: |
|
raise Exception(error) |
|
|
|
|
|
settings = payment.get_user_settings(chat_id) |
|
if settings[2] == "video" or isinstance(settings[2], MagicMock): |
|
|
|
convert_to_mp4(video_paths, bm) |
|
if settings[2] == "audio" or hijack == "bestaudio[ext=m4a]": |
|
convert_audio_format(video_paths, bm) |
|
|
|
return video_paths |
|
|
|
|
|
def convert_audio_format(video_paths: list, bm): |
|
|
|
|
|
|
|
|
|
for path in video_paths: |
|
streams = ffmpeg.probe(path)["streams"] |
|
if AUDIO_FORMAT is None and len(streams) == 1 and streams[0]["codec_type"] == "audio": |
|
logging.info("%s is audio, default format, no need to convert", path) |
|
elif AUDIO_FORMAT is None and len(streams) >= 2: |
|
logging.info("%s is video, default format, need to extract audio", path) |
|
audio_stream = {"codec_name": "m4a"} |
|
for stream in streams: |
|
if stream["codec_type"] == "audio": |
|
audio_stream = stream |
|
break |
|
ext = audio_stream["codec_name"] |
|
new_path = path.with_suffix(f".{ext}") |
|
run_ffmpeg_progressbar(["ffmpeg", "-y", "-i", path, "-vn", "-acodec", "copy", new_path], bm) |
|
path.unlink() |
|
index = video_paths.index(path) |
|
video_paths[index] = new_path |
|
else: |
|
logging.info("Not default format, converting %s to %s", path, AUDIO_FORMAT) |
|
new_path = path.with_suffix(f".{AUDIO_FORMAT}") |
|
run_ffmpeg_progressbar(["ffmpeg", "-y", "-i", path, new_path], bm) |
|
path.unlink() |
|
index = video_paths.index(path) |
|
video_paths[index] = new_path |
|
|
|
|
|
def split_large_video(video_paths: list): |
|
original_video = None |
|
split = False |
|
for original_video in video_paths: |
|
size = os.stat(original_video).st_size |
|
if size > TG_MAX_SIZE: |
|
split = True |
|
logging.warning("file is too large %s, splitting...", size) |
|
subprocess.check_output(f"sh split-video.sh {original_video} {TG_MAX_SIZE * 0.95} ".split()) |
|
os.remove(original_video) |
|
|
|
if split and original_video: |
|
return [i for i in pathlib.Path(original_video).parent.glob("*")] |
|
|
|
|
|
def download_instagram(url: str, tempdir: str): |
|
if not url.startswith("https://www.instagram.com"): |
|
return False |
|
|
|
resp = requests.get(f"http://192.168.6.1:15000/?url={url}").json() |
|
if url_results := resp.get("data"): |
|
for link in url_results: |
|
content = requests.get(link, stream=True).content |
|
ext = filetype.guess_extension(content) |
|
save_path = pathlib.Path(tempdir, f"{id(link)}.{ext}") |
|
with open(save_path, "wb") as f: |
|
f.write(content) |
|
|
|
return True |
|
|