diff --git a/bot/core/config_manager.py b/bot/core/config_manager.py index 0f38a713b..f12ef0889 100644 --- a/bot/core/config_manager.py +++ b/bot/core/config_manager.py @@ -12,9 +12,7 @@ class Config: DATABASE_URL = "" DEFAULT_UPLOAD = "rc" DOWNLOAD_DIR = "/usr/src/app/downloads/" - DELETE_LINKS = False EXTENSION_FILTER = "" - FSUB_IDS = "" FFMPEG_CMDS = {} FILELION_API = "" GDRIVE_ID = "" @@ -30,8 +28,6 @@ class Config: MEGA_PASSWORD = "" NAME_SUBSTITUTE = "" OWNER_ID = 0 - PAID_CHANNEL_ID = 0 - PAID_CHANNEL_LINK = "" QUEUE_ALL = 0 QUEUE_DOWNLOAD = 0 QUEUE_UPLOAD = 0 @@ -45,7 +41,6 @@ class Config: RSS_DELAY = 600 SEARCH_API_LINK = "" SEARCH_LIMIT = 0 - SET_COMMANDS = True SEARCH_PLUGINS: ClassVar[list[str]] = [] STOP_DUPLICATE = False STREAMWISH_API = "" @@ -54,7 +49,6 @@ class Config: TELEGRAM_HASH = "" THUMBNAIL_LAYOUT = "" TORRENT_TIMEOUT = 0 - TOKEN_TIMEOUT = 0 USER_TRANSMISSION = False UPSTREAM_REPO = "" UPSTREAM_BRANCH = "main" @@ -62,7 +56,16 @@ class Config: USE_SERVICE_ACCOUNTS = False WEB_PINCODE = False YT_DLP_OPTIONS = "" - + + # INKYPINKY + METADATA_KEY = "" + SET_COMMANDS = True + TOKEN_TIMEOUT = 0 + PAID_CHANNEL_ID = 0 + PAID_CHANNEL_LINK = "" + DELETE_LINKS = False + FSUB_IDS = "" + @classmethod def get(cls, key): if hasattr(cls, key): diff --git a/bot/helper/aeon_utils/metadata_editor.py b/bot/helper/aeon_utils/metadata_editor.py index 1d3179d7a..3f3b98cb8 100644 --- a/bot/helper/aeon_utils/metadata_editor.py +++ b/bot/helper/aeon_utils/metadata_editor.py @@ -6,10 +6,7 @@ from bot import LOGGER -async def change_metadata(file, key): - LOGGER.info(f"Starting metadata modification for file: {file}") - temp_file = f"{file}.temp.mkv" - +async def get_streams(file): cmd = [ "ffprobe", "-hide_banner", @@ -25,26 +22,36 @@ async def change_metadata(file, key): if process.returncode != 0: LOGGER.error(f"Error getting stream info: {stderr.decode().strip()}") - return + return None try: - streams = json.loads(stdout)["streams"] + return json.loads(stdout)["streams"] except KeyError: - LOGGER.error( - f"No streams found in the ffprobe output: {stdout.decode().strip()}", - ) + LOGGER.error(f"No streams found in the ffprobe output: {stdout.decode().strip()}") + return None + + +async def change_metadata(file, key): + LOGGER.info(f"Starting metadata modification for file: {file}") + temp_file = f"{file}.temp.mkv" + + streams = await get_streams(file) + if not streams: return languages = {} for stream in streams: stream_index = stream["index"] - stream_type = stream["codec_type"] if "tags" in stream and "language" in stream["tags"]: languages[stream_index] = stream["tags"]["language"] cmd = [ "xtra", - "-y", + "-hide_banner", + "-loglevel", + "error", + "-progress", + "pipe:1", "-i", file, "-map_metadata", @@ -175,4 +182,4 @@ async def add_attachment(file, attachment_path): os.replace(temp_file, file) LOGGER.info(f"Photo attachment added successfully to file: {file}") - return + return \ No newline at end of file diff --git a/bot/helper/common.py b/bot/helper/common.py index b415a0a6c..ec441853e 100644 --- a/bot/helper/common.py +++ b/bot/helper/common.py @@ -48,9 +48,13 @@ create_thumb, get_document_type, run_ffmpeg_cmd, + run_metadata_cmd, split_file, take_ss, + is_mkv, ) +from bot.helper.aeon_utils.metadata_editor import change_metadata, get_streams + from .mirror_leech_utils.gdrive_utils.list import GoogleDriveList from .mirror_leech_utils.rclone_utils.list import RcloneList from .mirror_leech_utils.status_utils.ffmpeg_status import FFmpegStatus @@ -1312,3 +1316,141 @@ async def proceed_ffmpeg(self, dl_path, gid): if checked: cpu_eater_lock.release() return dl_path + + async def proceed_metadata(self, up_dir, gid): + key = self.metadata + async with task_dict_lock: + task_dict[self.mid] = FFmpegStatus(self, gid, "Metadata") + checked = False + + async def process_file(file_path, key): + """Processes a single file to update metadata.""" + temp_file = f"{file_path}.temp.mkv" + streams = await get_streams(file_path) + if not streams: + return "" + + languages = { + stream["index"]: stream["tags"]["language"] + for stream in streams + if "tags" in stream and "language" in stream["tags"] + } + + cmd = [ + "xtra", + "-hide_banner", + "-loglevel", + "error", + "-progress", + "pipe:1", + "-i", + file_path, + "-map_metadata", + "-1", + "-c", + "copy", + "-metadata:s:v:0", + f"title={key}", + "-metadata", + f"title={key}", + ] + + audio_index = 0 + subtitle_index = 0 + first_video = False + + for stream in streams: + stream_index = stream["index"] + stream_type = stream["codec_type"] + + if stream_type == "video": + if not first_video: + cmd.extend(["-map", f"0:{stream_index}"]) + first_video = True + cmd.extend([f"-metadata:s:v:{stream_index}", f"title={key}"]) + if stream_index in languages: + cmd.extend( + [ + f"-metadata:s:v:{stream_index}", + f"language={languages[stream_index]}", + ] + ) + elif stream_type == "audio": + cmd.extend( + [ + "-map", + f"0:{stream_index}", + f"-metadata:s:a:{audio_index}", + f"title={key}", + ] + ) + if stream_index in languages: + cmd.extend( + [ + f"-metadata:s:a:{audio_index}", + f"language={languages[stream_index]}", + ] + ) + audio_index += 1 + elif stream_type == "subtitle": + codec_name = stream.get("codec_name", "unknown") + if codec_name in ["webvtt", "unknown"]: + LOGGER.warning( + f"Skipping unsupported subtitle metadata modification: {codec_name} for stream {stream_index}" + ) + else: + cmd.extend( + [ + "-map", + f"0:{stream_index}", + f"-metadata:s:s:{subtitle_index}", + f"title={key}", + ] + ) + if stream_index in languages: + cmd.extend( + [ + f"-metadata:s:s:{subtitle_index}", + f"language={languages[stream_index]}", + ] + ) + subtitle_index += 1 + else: + cmd.extend(["-map", f"0:{stream_index}"]) + + cmd.append(temp_file) + return cmd, temp_file + + async def process_directory(directory, key): + """Processes all MKV files in a directory.""" + for dirpath, _, files in await sync_to_async(walk, directory, topdown=False): + for file_ in files: + file_path = ospath.join(dirpath, file_) + if is_mkv(file_path): + cmd, temp_file = await process_file(file_path, key) + if not cmd: + return "" + if not checked: + await cpu_eater_lock.acquire() + if self.isCancelled: + cpu_eater_lock.release() + return "" + await run_metadata_cmd(self, cmd) + os.replace(temp_file, file_path) + + + if self.is_file: + if is_mkv(up_dir): + cmd, temp_file = await process_file(up_dir, key) + if cmd: + checked = True + await cpu_eater_lock.acquire() + await run_metadata_cmd(self, cmd) + os.replace(temp_file, up_dir) + cpu_eater_lock.release() + else: + await process_directory(up_dir, key) + + if checked: + cpu_eater_lock.release() + return up_dir \ No newline at end of file diff --git a/bot/helper/ext_utils/media_utils.py b/bot/helper/ext_utils/media_utils.py index ba21c5bc7..dc3b4787b 100644 --- a/bot/helper/ext_utils/media_utils.py +++ b/bot/helper/ext_utils/media_utils.py @@ -766,10 +766,10 @@ async def create_sample_video(listener, video_file, sample_duration, part_durati return None -async def run_ffmpeg_cmd(listener, ffmpeg, path): +async def run_ffmpeg_cmd(listener, cmd, path): base_name, ext = ospath.splitext(path) dir, base_name = base_name.rsplit("/", 1) - output_file = ffmpeg[-1] + output_file = cmd[-1] if output_file != "mltb" and output_file.startswith("mltb"): oext = ospath.splitext(output_file)[-1] if ext == oext: @@ -779,12 +779,12 @@ async def run_ffmpeg_cmd(listener, ffmpeg, path): else: base_name = f"ffmpeg.{base_name}" output = f"{dir}/{base_name}{ext}" - ffmpeg[-1] = output + cmd[-1] = output if listener.is_cancelled: return False async with listener.subprocess_lock: listener.subproc = await create_subprocess_exec( - *ffmpeg, stdout=PIPE, stderr=PIPE + *cmd, stdout=PIPE, stderr=PIPE ) code = await listener.subproc.wait() async with listener.subprocess_lock: @@ -805,3 +805,47 @@ async def run_ffmpeg_cmd(listener, ffmpeg, path): if await aiopath.exists(output): await remove(output) return False + + +async def run_metadata_cmd(listener, cmd): + #base_name, ext = ospath.splitext(path) + #dir, base_name = base_name.rsplit("/", 1) + #output_file = cmd[-1] + #if output_file != "mltb" and output_file.startswith("mltb"): + # oext = ospath.splitext(output_file)[-1] + # if ext == oext: + # base_name = f"ffmpeg.{base_name}" + # else: + # ext = oext + #else: + # base_name = f"ffmpeg.{base_name}" + # output = f"{dir}/{base_name}{ext}" + # cmd[-1] = output + if listener.is_cancelled: + return False + async with listener.subprocess_lock: + listener.subproc = await create_subprocess_exec( + *cmd, stdout=PIPE, stderr=PIPE + ) + code = await listener.subproc.wait() + async with listener.subprocess_lock: + if listener.is_cancelled: + return False + if code == 0: + return output + if code == -9: + listener.is_cancelled = True + return False + try: + stderr = (await listener.subproc.stderr.read()).decode().strip() + except Exception: + stderr = "Unable to decode the error!" + LOGGER.error( + f"{stderr}. Something went wrong while running metadata cmd, mostly file requires different/specific arguments.", + ) + # if await aiopath.exists(output): + # await remove(output) + return False + +def is_mkv(file): + return file.lower().endswith(".mkv") diff --git a/bot/helper/ext_utils/status_utils.py b/bot/helper/ext_utils/status_utils.py index 2d5fa9dfc..1f13e5c33 100644 --- a/bot/helper/ext_utils/status_utils.py +++ b/bot/helper/ext_utils/status_utils.py @@ -29,6 +29,7 @@ class MirrorStatus: STATUS_SAMVID = "SamVid" STATUS_CONVERT = "Convert" STATUS_FFMPEG = "FFmpeg" + STATUS_METADATA = "Metadata" STATUSES = { diff --git a/bot/helper/mirror_leech_utils/status_utils/ffmpeg_status.py b/bot/helper/mirror_leech_utils/status_utils/ffmpeg_status.py index 4bbb972a2..d26b9a890 100644 --- a/bot/helper/mirror_leech_utils/status_utils/ffmpeg_status.py +++ b/bot/helper/mirror_leech_utils/status_utils/ffmpeg_status.py @@ -72,7 +72,7 @@ def eta(self): self.listener.subsize - self._processed_bytes ) / self._speed_raw return get_readable_time(seconds) - except: + except Exception: return "-" def status(self): @@ -82,6 +82,8 @@ def status(self): return MirrorStatus.STATUS_SPLIT if self.cstatus == "Sample Video": return MirrorStatus.STATUS_SAMVID + if self.cstatus == "Metadata": + return MirrorStatus.STATUS_METADATA return MirrorStatus.STATUS_FFMPEG def task(self): diff --git a/bot/helper/mirror_leech_utils/status_utils/sevenz_status.py b/bot/helper/mirror_leech_utils/status_utils/sevenz_status.py index df3b0b812..41e6456ec 100644 --- a/bot/helper/mirror_leech_utils/status_utils/sevenz_status.py +++ b/bot/helper/mirror_leech_utils/status_utils/sevenz_status.py @@ -49,7 +49,7 @@ async def _sevenz_progress(self): self._processed_bytes = ( int(self._progress_str.strip("%")) / 100 ) * self.listener.subsize - except: + except Exception: self._processed_bytes = 0 self._progress_str = "0%" s = b"" @@ -86,7 +86,7 @@ def eta(self): self.listener.subsize - self._processed_bytes ) / self._speed_raw() return get_readable_time(seconds) - except: + except Exception: return "-" def status(self): @@ -94,9 +94,6 @@ def status(self): return MirrorStatus.STATUS_EXTRACT return MirrorStatus.STATUS_ARCHIVE - def processed_bytes(self): - return get_readable_file_size(self._processed_bytes) - def task(self): return self diff --git a/bot/modules/users_settings.py b/bot/modules/users_settings.py index 7506575c2..9e2deaee7 100644 --- a/bot/modules/users_settings.py +++ b/bot/modules/users_settings.py @@ -51,6 +51,7 @@ async def get_user_settings(from_user): index = user_dict.get("index_url", "None") upload_paths = "Added" if user_dict.get("upload_paths", False) else "None" ex_ex = user_dict.get("excluded_extensions", extension_filter or "None") + meta_msg = user_dict.get("metadata", Config.METADATA_KEY or "None") ns_msg = "Added" if user_dict.get("name_sub", False) else "None" ytopt = user_dict.get("yt_opt", Config.YT_DLP_OPTIONS or "None") ffc = user_dict.get("ffmpeg_cmds", Config.FFMPEG_CMDS or "None") @@ -103,6 +104,7 @@ async def get_user_settings(from_user): f"userset {user_id} user_tokens {user_tokens}", ) buttons.data_button("Excluded Extensions", f"userset {user_id} ex_ex") + buttons.data_button("Metadata key", f"userset {user_id} metadata_key") buttons.data_button("Name Subtitute", f"userset {user_id} name_substitute") buttons.data_button("YT-DLP Options", f"userset {user_id} yto") buttons.data_button("Ffmpeg Cmds", f"userset {user_id} ffc") @@ -132,6 +134,7 @@ async def get_user_settings(from_user): f"Default Package is {du}\n" f"Upload using {tr} token/config\n" f"Name substitution is {ns_msg}\n" + f"Metadata key is {meta_msg}\n" f"Excluded Extensions is {ex_ex}\n" f"YT-DLP Options is {escape(ytopt)}\n" f"FFMPEG Commands is {ffc}" @@ -327,6 +330,7 @@ async def edit_user_settings(client, query): "index_url", "excluded_extensions", "name_sub", + "metadata", "thumb_layout", "ffmpeg_cmds", "session_string", @@ -753,6 +757,20 @@ async def edit_user_settings(client, query): ) pfunc = partial(set_option, pre_event=query, option="name_sub") await event_handler(client, query, pfunc) + elif data[2] == "metadata_key": + await query.answer() + buttons = ButtonMaker() + if user_dict.get("metadata", False): + buttons.data_button("Remove Metadata key", f"userset {user_id} metadata") + buttons.data_button("Back", f"userset {user_id} back") + buttons.data_button("Close", f"userset {user_id} close") + emsg = "Metadata will change MKV video files including all audio, streams, and subtitle titles." + emsg += ( + f"Your Current Value is {user_dict.get('metadata') or 'not added yet!'}" + ) + await edit_message(message, emsg, buttons.build_menu(1), markdown=True) + pfunc = partial(set_option, pre_event=query, option="metadata") + await event_handler(client, query, pfunc) elif data[2] in ["gd", "rc"]: await query.answer() du = "rc" if data[2] == "gd" else "gd" diff --git a/config_sample.py b/config_sample.py index 26039e14e..15e70a661 100644 --- a/config_sample.py +++ b/config_sample.py @@ -30,6 +30,7 @@ PAID_CHANNEL_ID = 0 PAID_CHANNEL_LINK = "" SET_COMMANDS = True +METADATA_KEY = "" # GDrive Tools GDRIVE_ID = ""