From ed53f8a70a0e393ce31187c6ec5b93cf509a2e37 Mon Sep 17 00:00:00 2001 From: levi-s874 Date: Mon, 22 Dec 2025 14:00:40 +0100 Subject: [PATCH] Replace youtubesearchpython for yt-dlp --- .env.sample | 12 -- .gitignore | 15 ++- pyUltroid/fns/helper.py | 11 +- pyUltroid/fns/ytdl.py | 83 ++++++++----- pyUltroid/fns/ytdlOG.py | 265 ++++++++++++++++++++++++++++++++++++++++ ssgen.py | 173 ++++++++++++++++++++++++++ 6 files changed, 515 insertions(+), 44 deletions(-) delete mode 100644 .env.sample create mode 100644 pyUltroid/fns/ytdlOG.py create mode 100644 ssgen.py diff --git a/.env.sample b/.env.sample deleted file mode 100644 index 79d989c855..0000000000 --- a/.env.sample +++ /dev/null @@ -1,12 +0,0 @@ -# Don't use quotes( " and ' ) - -API_ID= -API_HASH= -SESSION= -REDIS_URI= -REDIS_PASSWORD= - -# [OPTIONAL] - -LOG_CHANNEL= -BOT_TOKEN= diff --git a/.gitignore b/.gitignore index ce5d8ae1e0..dfe9d120b0 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,17 @@ bin-release/ *.raw # fly.io configs -fly.toml \ No newline at end of file +fly.toml + +# Virtual environments +venv/ +.env/ +.venv/ + +# Python cache +__pycache__/ +*.pyc + +# IDEs +.vscode/ +.idea/ \ No newline at end of file diff --git a/pyUltroid/fns/helper.py b/pyUltroid/fns/helper.py index 105208bd13..3b0e375473 100644 --- a/pyUltroid/fns/helper.py +++ b/pyUltroid/fns/helper.py @@ -300,11 +300,20 @@ async def updater(): ac_br = repo.active_branch.name repo.create_remote("upstream", off_repo) if "upstream" not in repo.remotes else None ups_rem = repo.remote("upstream") - ups_rem.fetch(ac_br) + + try: + ups_rem.fetch(ac_br) + except GitCommandError: + LOGS.warning( + f"Updater skipped: branch '{ac_br}' not found in upstream" + ) + return False + changelog, tl_chnglog = await gen_chlog(repo, f"HEAD..upstream/{ac_br}") return bool(changelog) + # ----------------Fast Upload/Download---------------- # @1danish_00 @new-dev0 @buddhhu diff --git a/pyUltroid/fns/ytdl.py b/pyUltroid/fns/ytdl.py index c4d97d107d..a874817b11 100644 --- a/pyUltroid/fns/ytdl.py +++ b/pyUltroid/fns/ytdl.py @@ -1,22 +1,9 @@ -# Ultroid - UserBot -# Copyright (C) 2021-2025 TeamUltroid -# -# This file is a part of < https://github.com/TeamUltroid/Ultroid/ > -# PLease read the GNU Affero General Public License in -# . - import glob import os import re import time from telethon import Button - -try: - from youtubesearchpython import Playlist, VideosSearch -except ImportError: - Playlist, VideosSearch = None, None - from yt_dlp import YoutubeDL from .. import LOGS, udB @@ -24,6 +11,7 @@ from .tools import set_attributes +# ------------ Progreso de descarga ------------ async def ytdl_progress(k, start_time, event): if k["status"] == "error": return await event.edit("error") @@ -42,14 +30,26 @@ async def ytdl_progress(k, start_time, event): LOGS.error(f"ytdl_progress: {ex}") +# ------------ Obtener el enlace de YouTube (ahora con yt-dlp) ------------ def get_yt_link(query): - search = VideosSearch(query, limit=1).result() + ydl_opts = { + "quiet": True, + "default_search": "ytsearch1", # Búsqueda en YouTube + "skip_download": True, # No descargamos, solo extraemos la URL + "nocheckcertificate": True, # Desactivar la verificación del certificado SSL + } + try: - return search["result"][0]["link"] - except IndexError: - return + with YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(query, download=False) + if "entries" in info and info["entries"]: + return info["entries"][0].get("webpage_url") + except Exception as e: + LOGS.error(f"Error al obtener el enlace: {e}") + return None +# ------------ Descargar el video o playlist ------------ async def download_yt(event, link, ytd): reply_to = event.reply_to_msg_id or event info = await dler(event, link, ytd, download=True) @@ -162,10 +162,25 @@ async def download_yt(event, link, ytd): pass -# ---------------YouTube Downloader Inline--------------- -# @New-Dev0 @buddhhu @1danish-00 +# --------------- Obtener enlaces de videos en una playlist (usando yt-dlp) --------------- +async def get_videos_link(url): + to_return = [] + + ydl_opts = { + "quiet": True, + "extract_flat": True, # Esto solo extrae los enlaces sin descargarlos + } + + with YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=False) + if 'entries' in info: + for entry in info['entries']: + to_return.append(entry['url']) + + return to_return +# --------------- Obtener los botones de calidad de video y audio --------------- def get_formats(type, id, data): if type == "audio": audio = [] @@ -209,6 +224,7 @@ def get_formats(type, id, data): return [] +# --------------- Crear botones para selección de formatos --------------- def get_buttons(listt): id = listt[0]["ytid"] butts = [ @@ -227,6 +243,7 @@ def get_buttons(listt): return buttons +# --------------- Función para obtener el enlace del video --------------- async def dler(event, url, opts: dict = {}, download=False): await event.edit("`Getting Data...`") if "quiet" not in opts: @@ -242,6 +259,7 @@ async def dler(event, url, opts: dict = {}, download=False): return +# --------------- Descargar el video usando yt-dlp --------------- @run_async def ytdownload(url, opts): try: @@ -250,20 +268,25 @@ def ytdownload(url, opts): LOGS.error(ex) +# --------------- Extraer información del video --------------- @run_async def extract_info(url, opts): return YoutubeDL(opts).extract_info(url=url, download=False) -@run_async -def get_videos_link(url): +# --------------- Obtener los enlaces de los videos de la playlist --------------- +async def get_videos_link(url): to_return = [] - regex = re.search(r"\?list=([(\w+)\-]*)", url) - if not regex: - return to_return - playlist_id = regex.group(1) - videos = Playlist(playlist_id) - for vid in videos.videos: - link = re.search(r"\?v=([(\w+)\-]*)", vid["link"]).group(1) - to_return.append(f"https://youtube.com/watch?v={link}") - return to_return + + ydl_opts = { + "quiet": True, + "extract_flat": True, # Esto solo extrae los enlaces sin descargarlos + } + + with YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=False) + if 'entries' in info: + for entry in info['entries']: + to_return.append(entry['url']) + + return to_return \ No newline at end of file diff --git a/pyUltroid/fns/ytdlOG.py b/pyUltroid/fns/ytdlOG.py new file mode 100644 index 0000000000..68c948b84a --- /dev/null +++ b/pyUltroid/fns/ytdlOG.py @@ -0,0 +1,265 @@ +# Ultroid - UserBot +# Copyright (C) 2021-2025 TeamUltroid +# +# This file is a part of < https://github.com/TeamUltroid/Ultroid/ > +# PLease read the GNU Affero General Public License in +# . + +import glob +import os +import re +import time + +from telethon import Button + +from yt_dlp import YoutubeDL + +from .. import LOGS, udB +from .helper import download_file, humanbytes, run_async, time_formatter +from .tools import set_attributes + + + +async def ytdl_progress(k, start_time, event): + if k["status"] == "error": + return await event.edit("error") + while k["status"] == "downloading": + text = ( + f"`Downloading: {k['filename']}\n" + + f"Total Size: {humanbytes(k['total_bytes'])}\n" + + f"Downloaded: {humanbytes(k['downloaded_bytes'])}\n" + + f"Speed: {humanbytes(k['speed'])}/s\n" + + f"ETA: {time_formatter(k['eta']*1000)}`" + ) + if round((time.time() - start_time) % 10.0) == 0: + try: + await event.edit(text) + except Exception as ex: + LOGS.error(f"ytdl_progress: {ex}") + + +def get_yt_link(query): + search = VideosSearch(query, limit=1).result() + try: + return search["result"][0]["link"] + except IndexError: + return + + +async def download_yt(event, link, ytd): + reply_to = event.reply_to_msg_id or event + info = await dler(event, link, ytd, download=True) + if not info: + return + if info.get("_type", None) == "playlist": + total = info["playlist_count"] + for num, file in enumerate(info["entries"]): + num += 1 + id_ = file["id"] + thumb = id_ + ".jpg" + title = file["title"] + await download_file( + file.get("thumbnail", None) or file["thumbnails"][-1]["url"], thumb + ) + ext = "." + ytd["outtmpl"]["default"].split(".")[-1] + if ext == ".m4a": + ext = ".mp3" + id = None + for x in glob.glob(f"{id_}*"): + if not x.endswith("jpg"): + id = x + if not id: + return + ext = "." + id.split(".")[-1] + file = title + ext + try: + os.rename(id, file) + except FileNotFoundError: + try: + os.rename(id + ext, file) + except FileNotFoundError as er: + if os.path.exists(id): + file = id + else: + raise er + if file.endswith(".part"): + os.remove(file) + os.remove(thumb) + await event.client.send_message( + event.chat_id, + f"`[{num}/{total}]` `Invalid Video format.\nIgnoring that...`", + ) + return + attributes = await set_attributes(file) + res, _ = await event.client.fast_uploader( + file, show_progress=True, event=event, to_delete=True + ) + from_ = info["extractor"].split(":")[0] + caption = f"`[{num}/{total}]` `{title}`\n\n`from {from_}`" + await event.client.send_file( + event.chat_id, + file=res, + caption=caption, + attributes=attributes, + supports_streaming=True, + thumb=thumb, + reply_to=reply_to, + ) + os.remove(thumb) + try: + await event.delete() + except BaseException: + pass + return + title = info["title"] + if len(title) > 20: + title = title[:17] + "..." + id_ = info["id"] + thumb = id_ + ".jpg" + await download_file( + info.get("thumbnail", None) or f"https://i.ytimg.com/vi/{id_}/hqdefault.jpg", + thumb, + ) + ext = "." + ytd["outtmpl"]["default"].split(".")[-1] + for _ext in [".m4a", ".mp3", ".opus"]: + if ext == _ext: + ext = _ext + break + id = None + for x in glob.glob(f"{id_}*"): + if not x.endswith("jpg"): + id = x + if not id: + return + ext = "." + id.split(".")[-1] + file = title + ext + try: + os.rename(id, file) + except FileNotFoundError: + os.rename(id + ext, file) + attributes = await set_attributes(file) + res, _ = await event.client.fast_uploader( + file, show_progress=True, event=event, to_delete=True + ) + caption = f"`{info['title']}`" + await event.client.send_file( + event.chat_id, + file=res, + caption=caption, + attributes=attributes, + supports_streaming=True, + thumb=thumb, + reply_to=reply_to, + ) + os.remove(thumb) + try: + await event.delete() + except BaseException: + pass + + +# ---------------YouTube Downloader Inline--------------- +# @New-Dev0 @buddhhu @1danish-00 + + +def get_formats(type, id, data): + if type == "audio": + audio = [] + for _quality in ["64", "128", "256", "320"]: + _audio = {} + _audio.update( + { + "ytid": id, + "type": "audio", + "id": _quality, + "quality": _quality + "KBPS", + } + ) + audio.append(_audio) + return audio + if type == "video": + video = [] + size = 0 + for vid in data["formats"]: + if vid["format_id"] == "251": + size += vid["filesize"] if vid.get("filesize") else 0 + if vid["vcodec"] != "none": + _id = int(vid["format_id"]) + _quality = str(vid["width"]) + "×" + str(vid["height"]) + _size = size + (vid["filesize"] if vid.get("filesize") else 0) + _ext = "mkv" if vid["ext"] == "webm" else "mp4" + if _size < 2147483648: # Telegram's Limit of 2GB + _video = {} + _video.update( + { + "ytid": id, + "type": "video", + "id": str(_id) + "+251", + "quality": _quality, + "size": _size, + "ext": _ext, + } + ) + video.append(_video) + return video + return [] + + +def get_buttons(listt): + id = listt[0]["ytid"] + butts = [ + Button.inline( + text=f"[{x['quality']}" + + (f" {humanbytes(x['size'])}]" if x.get("size") else "]"), + data=f"ytdownload:{x['type']}:{x['id']}:{x['ytid']}" + + (f":{x['ext']}" if x.get("ext") else ""), + ) + for x in listt + ] + buttons = list(zip(butts[::2], butts[1::2])) + if len(butts) % 2 == 1: + buttons.append((butts[-1],)) + buttons.append([Button.inline("« Back", f"ytdl_back:{id}")]) + return buttons + + +async def dler(event, url, opts: dict = {}, download=False): + await event.edit("`Getting Data...`") + if "quiet" not in opts: + opts["quiet"] = True + opts["username"] = udB.get_key("YT_USERNAME") + opts["password"] = udB.get_key("YT_PASSWORD") + if download: + await ytdownload(url, opts) + try: + return await extract_info(url, opts) + except Exception as e: + await event.edit(f"{type(e)}: {e}") + return + + +@run_async +def ytdownload(url, opts): + try: + return YoutubeDL(opts).download([url]) + except Exception as ex: + LOGS.error(ex) + + +@run_async +def extract_info(url, opts): + return YoutubeDL(opts).extract_info(url=url, download=False) + + +@run_async +def get_videos_link(url): + to_return = [] + regex = re.search(r"\?list=([(\w+)\-]*)", url) + if not regex: + return to_return + playlist_id = regex.group(1) + videos = Playlist(playlist_id) + for vid in videos.videos: + link = re.search(r"\?v=([(\w+)\-]*)", vid["link"]).group(1) + to_return.append(f"https://youtube.com/watch?v={link}") + return to_return diff --git a/ssgen.py b/ssgen.py new file mode 100644 index 0000000000..e3522cf150 --- /dev/null +++ b/ssgen.py @@ -0,0 +1,173 @@ +#!/usr/bin/python3 +# Ultroid - UserBot +# Copyright (C) 2021-2025 TeamUltroid +# +# This file is a part of < https://github.com/TeamUltroid/Ultroid/ > +# PLease read the GNU Affero General Public License in +# . + +import os +from time import sleep + +ULTROID = r""" + _ _ _ _ _ _ + | | | | | | (_) | | + | | | | | |_ _ __ ___ _ __| | + | | | | | __| '__/ _ \| |/ _ | + | |__| | | |_| | | (_) | | (_| | + \____/|_|\__|_| \___/|_|\__,_| +""" + + +def spinner(x): + if x == "tele": + print("Checking if Telethon is installed...") + else: + print("Checking if Pyrogram is installed...") + for _ in range(3): + for frame in r"-\|/-\|/": + print("\b", frame, sep="", end="", flush=True) + sleep(0.1) + + +def clear_screen(): + # https://www.tutorialspoint.com/how-to-clear-screen-in-python#:~:text=In%20Python%20sometimes%20we%20have,screen%20by%20pressing%20Control%20%2B%20l%20. + if os.name == "posix": + os.system("clear") + else: + # for windows platfrom + os.system("cls") + + +def get_api_id_and_hash(): + print( + "Get your API ID and API HASH from my.telegram.org or @ScrapperRoBot to proceed.\n\n", + ) + try: + API_ID = int(input("Please enter your API ID: ")) + except ValueError: + print("APP ID must be an integer.\nQuitting...") + exit(0) + API_HASH = input("Please enter your API HASH: ") + return API_ID, API_HASH + + +def telethon_session(): + try: + spinner("tele") + import telethon + x = "\bFound an existing installation of Telethon...\nSuccessfully Imported.\n\n" + except ImportError: + print("Installing Telethon...") + os.system("pip uninstall telethon -y && pip install -U telethon") + + x = "\bDone. Installed and imported Telethon." + clear_screen() + print(ULTROID) + print(x) + + # the imports + + from telethon.errors.rpcerrorlist import ( + ApiIdInvalidError, + PhoneNumberInvalidError, + UserIsBotError, + ) + from telethon.sessions import StringSession + from telethon.sync import TelegramClient + + API_ID, API_HASH = get_api_id_and_hash() + + # logging in + try: + with TelegramClient(StringSession(), API_ID, API_HASH) as ultroid: + print("Generating a string session for •ULTROID•") + try: + ultroid.send_message( + "me", + f"**ULTROID** `SESSION`:\n\n`{ultroid.session.save()}`\n\n**Do not share this anywhere!**", + ) + print( + "Your SESSION has been generated. Check your Telegram saved messages!" + ) + return + except UserIsBotError: + print("You are trying to Generate Session for your Bot's Account?") + print("Here is That!\n{ultroid.session.save()}\n\n") + print("NOTE: You can't use that as User Session..") + except ApiIdInvalidError: + print( + "Your API ID/API HASH combination is invalid. Kindly recheck.\nQuitting..." + ) + exit(0) + except ValueError: + print("API HASH must not be empty!\nQuitting...") + exit(0) + except PhoneNumberInvalidError: + print("The phone number is invalid!\nQuitting...") + exit(0) + except Exception as er: + print("Unexpected Error Occurred while Creating Session") + print(er) + print("If you think It as a Bug, Report to @UltroidSupportChat.\n\n") + + +def pyro_session(): + try: + spinner("pyro") + from pyrogram import Client + + x = "\bFound an existing installation of Pyrogram...\nSuccessfully Imported.\n\n" + except BaseException: + print("Installing Pyrogram...") + os.system("pip install pyrogram tgcrypto") + x = "\bDone. Installed and imported Pyrogram." + from pyrogram import Client + + clear_screen() + print(ULTROID) + print(x) + + # generate a session + API_ID, API_HASH = get_api_id_and_hash() + print("Enter phone number when asked.\n\n") + try: + with Client(name="ultroid", api_id=API_ID, api_hash=API_HASH, in_memory=True) as pyro: + ss = pyro.export_session_string() + pyro.send_message( + "me", + f"`{ss}`\n\nAbove is your Pyrogram Session String for @TheUltroid. **DO NOT SHARE it.**", + ) + print("Session has been sent to your saved messages!") + exit(0) + except Exception as er: + print("Unexpected error occurred while creating session, make sure to validate your inputs.") + print(er) + + +def main(): + clear_screen() + print(ULTROID) + try: + type_of_ss = int( + input( + "\nUltroid supports both telethon as well as pyrogram sessions.\n\nWhich session do you want to generate?\n1. Telethon Session.\n2. Pyrogram Session.\n\nEnter choice: " + ) + ) + except Exception as e: + print(e) + exit(0) + if type_of_ss == 1: + telethon_session() + elif type_of_ss == 2: + pyro_session() + else: + print("Invalid choice.") + x = input("Run again? (Y/n)") + if x.lower() in ["y", "yes"]: + main() + else: + exit(0) + + +main()