Path: blob/master/bot/helper/ext_utils/media_utils.py
1635 views
from PIL import Image1from aiofiles.os import remove, path as aiopath, makedirs2from asyncio import (3create_subprocess_exec,4gather,5wait_for,6)7from asyncio.subprocess import PIPE8from os import path as ospath9from re import search as re_search, escape10from time import time11from aioshutil import rmtree1213from ... import LOGGER, DOWNLOAD_DIR, threads, cores14from .bot_utils import cmd_exec, sync_to_async15from .files_utils import get_mime_type, is_archive, is_archive_split16from .status_utils import time_to_seconds171819async def create_thumb(msg, _id=""):20if not _id:21_id = time()22path = f"{DOWNLOAD_DIR}thumbnails"23else:24path = "thumbnails"25await makedirs(path, exist_ok=True)26photo_dir = await msg.download()27output = ospath.join(path, f"{_id}.jpg")28await sync_to_async(Image.open(photo_dir).convert("RGB").save, output, "JPEG")29await remove(photo_dir)30return output313233async def get_media_info(path):34try:35result = await cmd_exec(36[37"ffprobe",38"-hide_banner",39"-loglevel",40"error",41"-print_format",42"json",43"-show_format",44path,45]46)47except Exception as e:48LOGGER.error(f"Get Media Info: {e}. Mostly File not found! - File: {path}")49return 0, None, None50if result[0] and result[2] == 0:51fields = eval(result[0]).get("format")52if fields is None:53LOGGER.error(f"get_media_info: {result}")54return 0, None, None55duration = round(float(fields.get("duration", 0)))56tags = fields.get("tags", {})57artist = tags.get("artist") or tags.get("ARTIST") or tags.get("Artist")58title = tags.get("title") or tags.get("TITLE") or tags.get("Title")59return duration, artist, title60return 0, None, None616263async def get_document_type(path):64is_video, is_audio, is_image = False, False, False65if (66is_archive(path)67or is_archive_split(path)68or re_search(r".+(\.|_)(rar|7z|zip|bin)(\.0*\d+)?$", path)69):70return is_video, is_audio, is_image71mime_type = await sync_to_async(get_mime_type, path)72if mime_type.startswith("image"):73return False, False, True74try:75result = await cmd_exec(76[77"ffprobe",78"-hide_banner",79"-loglevel",80"error",81"-print_format",82"json",83"-show_streams",84path,85]86)87if result[1] and mime_type.startswith("video"):88is_video = True89except Exception as e:90LOGGER.error(f"Get Document Type: {e}. Mostly File not found! - File: {path}")91if mime_type.startswith("audio"):92return False, True, False93if not mime_type.startswith("video") and not mime_type.endswith("octet-stream"):94return is_video, is_audio, is_image95if mime_type.startswith("video"):96is_video = True97return is_video, is_audio, is_image98if result[0] and result[2] == 0:99fields = eval(result[0]).get("streams")100if fields is None:101LOGGER.error(f"get_document_type: {result}")102return is_video, is_audio, is_image103is_video = False104for stream in fields:105if stream.get("codec_type") == "video":106codec_name = stream.get("codec_name", "").lower()107if codec_name not in {"mjpeg", "png", "bmp"}:108is_video = True109elif stream.get("codec_type") == "audio":110is_audio = True111return is_video, is_audio, is_image112113114async def take_ss(video_file, ss_nb) -> bool:115duration = (await get_media_info(video_file))[0]116if duration != 0:117dirpath, name = video_file.rsplit("/", 1)118name, _ = ospath.splitext(name)119dirpath = f"{dirpath}/{name}_mltbss"120await makedirs(dirpath, exist_ok=True)121interval = duration // (ss_nb + 1)122cap_time = interval123cmds = []124for i in range(ss_nb):125output = f"{dirpath}/SS.{name}_{i:02}.png"126cmd = [127"taskset",128"-c",129f"{cores}",130"ffmpeg",131"-hide_banner",132"-loglevel",133"error",134"-ss",135f"{cap_time}",136"-i",137video_file,138"-q:v",139"1",140"-frames:v",141"1",142"-threads",143f"{threads}",144output,145]146cap_time += interval147cmds.append(cmd_exec(cmd))148try:149resutls = await wait_for(gather(*cmds), timeout=60)150if resutls[0][2] != 0:151LOGGER.error(152f"Error while creating screenshots from video. Path: {video_file}. stderr: {resutls[0][1]}"153)154await rmtree(dirpath, ignore_errors=True)155return False156except:157LOGGER.error(158f"Error while creating screenshots from video. Path: {video_file}. Error: Timeout some issues with ffmpeg with specific arch!"159)160await rmtree(dirpath, ignore_errors=True)161return False162return dirpath163else:164LOGGER.error("take_ss: Can't get the duration of video")165return False166167168async def get_audio_thumbnail(audio_file):169output_dir = f"{DOWNLOAD_DIR}thumbnails"170await makedirs(output_dir, exist_ok=True)171output = ospath.join(output_dir, f"{time()}.jpg")172cmd = [173"taskset",174"-c",175f"{cores}",176"ffmpeg",177"-hide_banner",178"-loglevel",179"error",180"-i",181audio_file,182"-an",183"-vcodec",184"copy",185"-threads",186f"{threads}",187output,188]189try:190_, err, code = await wait_for(cmd_exec(cmd), timeout=60)191if code != 0 or not await aiopath.exists(output):192LOGGER.error(193f"Error while extracting thumbnail from audio. Name: {audio_file} stderr: {err}"194)195return None196except:197LOGGER.error(198f"Error while extracting thumbnail from audio. Name: {audio_file}. Error: Timeout some issues with ffmpeg with specific arch!"199)200return None201return output202203204async def get_video_thumbnail(video_file, duration):205output_dir = f"{DOWNLOAD_DIR}thumbnails"206await makedirs(output_dir, exist_ok=True)207output = ospath.join(output_dir, f"{time()}.jpg")208if duration is None:209duration = (await get_media_info(video_file))[0]210if duration == 0:211duration = 3212duration = duration // 2213cmd = [214"taskset",215"-c",216f"{cores}",217"ffmpeg",218"-hide_banner",219"-loglevel",220"error",221"-ss",222f"{duration}",223"-i",224video_file,225"-vf",226"thumbnail",227"-q:v",228"1",229"-frames:v",230"1",231"-threads",232f"{threads}",233output,234]235try:236_, err, code = await wait_for(cmd_exec(cmd), timeout=60)237if code != 0 or not await aiopath.exists(output):238LOGGER.error(239f"Error while extracting thumbnail from video. Name: {video_file} stderr: {err}"240)241return None242except:243LOGGER.error(244f"Error while extracting thumbnail from video. Name: {video_file}. Error: Timeout some issues with ffmpeg with specific arch!"245)246return None247return output248249250async def get_multiple_frames_thumbnail(video_file, layout, keep_screenshots):251ss_nb = layout.split("x")252ss_nb = int(ss_nb[0]) * int(ss_nb[1])253dirpath = await take_ss(video_file, ss_nb)254if not dirpath:255return None256output_dir = f"{DOWNLOAD_DIR}thumbnails"257await makedirs(output_dir, exist_ok=True)258output = ospath.join(output_dir, f"{time()}.jpg")259cmd = [260"taskset",261"-c",262f"{cores}",263"ffmpeg",264"-hide_banner",265"-loglevel",266"error",267"-pattern_type",268"glob",269"-i",270f"{escape(dirpath)}/*.png",271"-vf",272f"tile={layout}, thumbnail",273"-q:v",274"1",275"-frames:v",276"1",277"-f",278"mjpeg",279"-threads",280f"{threads}",281output,282]283try:284_, err, code = await wait_for(cmd_exec(cmd), timeout=60)285if code != 0 or not await aiopath.exists(output):286LOGGER.error(287f"Error while combining thumbnails for video. Name: {video_file} stderr: {err}"288)289return None290except:291LOGGER.error(292f"Error while combining thumbnails from video. Name: {video_file}. Error: Timeout some issues with ffmpeg with specific arch!"293)294return None295finally:296if not keep_screenshots:297await rmtree(dirpath, ignore_errors=True)298return output299300301class FFMpeg:302303def __init__(self, listener):304self._listener = listener305self._processed_bytes = 0306self._last_processed_bytes = 0307self._processed_time = 0308self._last_processed_time = 0309self._speed_raw = 0310self._progress_raw = 0311self._total_time = 0312self._eta_raw = 0313self._time_rate = 0.1314self._start_time = 0315316@property317def processed_bytes(self):318return self._processed_bytes319320@property321def speed_raw(self):322return self._speed_raw323324@property325def progress_raw(self):326return self._progress_raw327328@property329def eta_raw(self):330return self._eta_raw331332def clear(self):333self._start_time = time()334self._processed_bytes = 0335self._processed_time = 0336self._speed_raw = 0337self._progress_raw = 0338self._eta_raw = 0339self._time_rate = 0.1340self._last_processed_time = 0341self._last_processed_bytes = 0342343async def _ffmpeg_progress(self):344while not (345self._listener.subproc.returncode is not None346or self._listener.is_cancelled347or self._listener.subproc.stdout.at_eof()348):349try:350line = await wait_for(self._listener.subproc.stdout.readline(), 60)351except:352break353line = line.decode().strip()354if not line:355break356if "=" in line:357key, value = line.split("=", 1)358if value != "N/A":359if key == "total_size":360self._processed_bytes = int(value) + self._last_processed_bytes361self._speed_raw = self._processed_bytes / (362time() - self._start_time363)364elif key == "speed":365self._time_rate = max(0.1, float(value.strip("x")))366elif key == "out_time":367self._processed_time = (368time_to_seconds(value) + self._last_processed_time369)370try:371self._progress_raw = (372self._processed_time * 100373) / self._total_time374self._eta_raw = (375self._total_time - self._processed_time376) / self._time_rate377except:378self._progress_raw = 0379self._eta_raw = 0380381async def ffmpeg_cmds(self, ffmpeg, f_path):382self.clear()383self._total_time = (await get_media_info(f_path))[0]384base_name, ext = ospath.splitext(f_path)385dir, base_name = base_name.rsplit("/", 1)386indices = [387index388for index, item in enumerate(ffmpeg)389if item.startswith("mltb") or item == "mltb"390]391outputs = []392for index in indices:393output_file = ffmpeg[index]394if output_file != "mltb" and output_file.startswith("mltb"):395bo, oext = ospath.splitext(output_file)396if oext:397if ext == oext:398prefix = f"ffmpeg{index}." if bo == "mltb" else ""399else:400prefix = ""401ext = ""402else:403prefix = ""404else:405prefix = f"ffmpeg{index}."406output = f"{dir}/{prefix}{output_file.replace("mltb", base_name)}{ext}"407outputs.append(output)408ffmpeg[index] = output409if self._listener.is_cancelled:410return False411self._listener.subproc = await create_subprocess_exec(412*ffmpeg, stdout=PIPE, stderr=PIPE413)414await self._ffmpeg_progress()415_, stderr = await self._listener.subproc.communicate()416code = self._listener.subproc.returncode417if self._listener.is_cancelled:418return False419if code == 0:420return outputs421elif code == -9:422self._listener.is_cancelled = True423return False424else:425try:426stderr = stderr.decode().strip()427except:428stderr = "Unable to decode the error!"429LOGGER.error(430f"{stderr}. Something went wrong while running ffmpeg cmd, mostly file requires different/specific arguments. Path: {f_path}"431)432for op in outputs:433if await aiopath.exists(op):434await remove(op)435return False436437async def convert_video(self, video_file, ext, retry=False):438self.clear()439self._total_time = (await get_media_info(video_file))[0]440base_name = ospath.splitext(video_file)[0]441output = f"{base_name}.{ext}"442if retry:443cmd = [444"taskset",445"-c",446f"{cores}",447"ffmpeg",448"-hide_banner",449"-loglevel",450"error",451"-progress",452"pipe:1",453"-i",454video_file,455"-map",456"0",457"-c:v",458"libx264",459"-c:a",460"aac",461"-threads",462f"{threads}",463output,464]465if ext == "mp4":466cmd[17:17] = ["-c:s", "mov_text"]467elif ext == "mkv":468cmd[17:17] = ["-c:s", "ass"]469else:470cmd[17:17] = ["-c:s", "copy"]471else:472cmd = [473"taskset",474"-c",475f"{cores}",476"ffmpeg",477"-hide_banner",478"-loglevel",479"error",480"-progress",481"pipe:1",482"-i",483video_file,484"-map",485"0",486"-c",487"copy",488"-threads",489f"{threads}",490output,491]492if self._listener.is_cancelled:493return False494self._listener.subproc = await create_subprocess_exec(495*cmd, stdout=PIPE, stderr=PIPE496)497await self._ffmpeg_progress()498_, stderr = await self._listener.subproc.communicate()499code = self._listener.subproc.returncode500if self._listener.is_cancelled:501return False502if code == 0:503return output504elif code == -9:505self._listener.is_cancelled = True506return False507else:508if await aiopath.exists(output):509await remove(output)510if not retry:511return await self.convert_video(video_file, ext, True)512try:513stderr = stderr.decode().strip()514except:515stderr = "Unable to decode the error!"516LOGGER.error(517f"{stderr}. Something went wrong while converting video, mostly file need specific codec. Path: {video_file}"518)519return False520521async def convert_audio(self, audio_file, ext):522self.clear()523self._total_time = (await get_media_info(audio_file))[0]524base_name = ospath.splitext(audio_file)[0]525output = f"{base_name}.{ext}"526cmd = [527"taskset",528"-c",529f"{cores}",530"ffmpeg",531"-hide_banner",532"-loglevel",533"error",534"-progress",535"pipe:1",536"-i",537audio_file,538"-threads",539f"{threads}",540output,541]542if self._listener.is_cancelled:543return False544self._listener.subproc = await create_subprocess_exec(545*cmd, stdout=PIPE, stderr=PIPE546)547await self._ffmpeg_progress()548_, stderr = await self._listener.subproc.communicate()549code = self._listener.subproc.returncode550if self._listener.is_cancelled:551return False552if code == 0:553return output554elif code == -9:555self._listener.is_cancelled = True556return False557else:558try:559stderr = stderr.decode().strip()560except:561stderr = "Unable to decode the error!"562LOGGER.error(563f"{stderr}. Something went wrong while converting audio, mostly file need specific codec. Path: {audio_file}"564)565if await aiopath.exists(output):566await remove(output)567return False568569async def sample_video(self, video_file, sample_duration, part_duration):570self.clear()571self._total_time = sample_duration572dir, name = video_file.rsplit("/", 1)573output_file = f"{dir}/SAMPLE.{name}"574segments = [(0, part_duration)]575duration = (await get_media_info(video_file))[0]576remaining_duration = duration - (part_duration * 2)577parts = (sample_duration - (part_duration * 2)) // part_duration578time_interval = remaining_duration // parts579next_segment = time_interval580for _ in range(parts):581segments.append((next_segment, next_segment + part_duration))582next_segment += time_interval583segments.append((duration - part_duration, duration))584585filter_complex = ""586for i, (start, end) in enumerate(segments):587filter_complex += (588f"[0:v]trim=start={start}:end={end},setpts=PTS-STARTPTS[v{i}]; "589)590filter_complex += (591f"[0:a]atrim=start={start}:end={end},asetpts=PTS-STARTPTS[a{i}]; "592)593594for i in range(len(segments)):595filter_complex += f"[v{i}][a{i}]"596597filter_complex += f"concat=n={len(segments)}:v=1:a=1[vout][aout]"598599cmd = [600"taskset",601"-c",602f"{cores}",603"ffmpeg",604"-hide_banner",605"-loglevel",606"error",607"-progress",608"pipe:1",609"-i",610video_file,611"-filter_complex",612filter_complex,613"-map",614"[vout]",615"-map",616"[aout]",617"-c:v",618"libx264",619"-c:a",620"aac",621"-threads",622f"{threads}",623output_file,624]625626if self._listener.is_cancelled:627return False628self._listener.subproc = await create_subprocess_exec(629*cmd, stdout=PIPE, stderr=PIPE630)631await self._ffmpeg_progress()632_, stderr = await self._listener.subproc.communicate()633code = self._listener.subproc.returncode634if self._listener.is_cancelled:635return False636if code == -9:637self._listener.is_cancelled = True638return False639elif code == 0:640return output_file641else:642try:643stderr = stderr.decode().strip()644except Exception:645stderr = "Unable to decode the error!"646LOGGER.error(647f"{stderr}. Something went wrong while creating sample video, mostly file is corrupted. Path: {video_file}"648)649if await aiopath.exists(output_file):650await remove(output_file)651return False652653async def split(self, f_path, file_, parts, split_size):654self.clear()655multi_streams = True656self._total_time = duration = (await get_media_info(f_path))[0]657base_name, extension = ospath.splitext(file_)658split_size -= 3000000659start_time = 0660i = 1661while i <= parts or start_time < duration - 4:662out_path = f_path.replace(file_, f"{base_name}.part{i:03}{extension}")663cmd = [664"taskset",665"-c",666f"{cores}",667"ffmpeg",668"-hide_banner",669"-loglevel",670"error",671"-progress",672"pipe:1",673"-ss",674str(start_time),675"-i",676f_path,677"-fs",678str(split_size),679"-map",680"0",681"-map_chapters",682"-1",683"-async",684"1",685"-strict",686"-2",687"-c",688"copy",689"-threads",690f"{threads}",691out_path,692]693if not multi_streams:694del cmd[15]695del cmd[15]696if self._listener.is_cancelled:697return False698self._listener.subproc = await create_subprocess_exec(699*cmd, stdout=PIPE, stderr=PIPE700)701await self._ffmpeg_progress()702_, stderr = await self._listener.subproc.communicate()703code = self._listener.subproc.returncode704if self._listener.is_cancelled:705return False706if code == -9:707self._listener.is_cancelled = True708return False709elif code != 0:710try:711stderr = stderr.decode().strip()712except:713stderr = "Unable to decode the error!"714try:715await remove(out_path)716except:717pass718if multi_streams:719LOGGER.warning(720f"{stderr}. Retrying without map, -map 0 not working in all situations. Path: {f_path}"721)722multi_streams = False723continue724else:725LOGGER.warning(726f"{stderr}. Unable to split this video, if it's size less than {self._listener.max_split_size} will be uploaded as it is. Path: {f_path}"727)728return False729out_size = await aiopath.getsize(out_path)730if out_size > self._listener.max_split_size:731split_size -= (out_size - self._listener.max_split_size) + 5000000732LOGGER.warning(733f"Part size is {out_size}. Trying again with lower split size!. Path: {f_path}"734)735await remove(out_path)736continue737lpd = (await get_media_info(out_path))[0]738if lpd == 0:739LOGGER.error(740f"Something went wrong while splitting, mostly file is corrupted. Path: {f_path}"741)742break743elif duration == lpd:744LOGGER.warning(745f"This file has been splitted with default stream and audio, so you will only see one part with less size from original one because it doesn't have all streams and audios. This happens mostly with MKV videos. Path: {f_path}"746)747break748elif lpd <= 3:749await remove(out_path)750break751self._last_processed_time += lpd752self._last_processed_bytes += out_size753start_time += lpd - 3754i += 1755return True756757758759