From e1c52f2f7c0ac845f001c74f9a290aedc9abd93d Mon Sep 17 00:00:00 2001 From: Savsis Date: Sat, 25 Jul 2026 10:37:57 +0500 Subject: [PATCH] Add files via upload --- deezer_dl.py | 285 +++++++++++++++++++++++++++++++++++++++++++++++ soundcloud_dl.py | 207 ++++++++++++++++++++++++++++++++++ start.py | 112 +++++++++++++++++++ youtube_dl.py | 217 ++++++++++++++++++++++++++++++++++++ 4 files changed, 821 insertions(+) create mode 100644 deezer_dl.py create mode 100644 soundcloud_dl.py create mode 100644 start.py create mode 100644 youtube_dl.py diff --git a/deezer_dl.py b/deezer_dl.py new file mode 100644 index 0000000..fb78c7a --- /dev/null +++ b/deezer_dl.py @@ -0,0 +1,285 @@ +import subprocess +import sys +import os +import re +import zipfile +import shutil +import urllib.request +import urllib.parse +import json +import webbrowser +import time +import msvcrt + +os.system('') + +def pip_install(pkg): + subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "--upgrade", pkg], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +try: + import yt_dlp +except ImportError: + print("Устанавливаю yt-dlp...") + pip_install("yt-dlp") + import yt_dlp + +try: + import requests +except ImportError: + pip_install("requests") + import requests + +FFMPEG_DIR = os.path.join(os.path.expanduser("~"), ".local_ffmpeg") +FFMPEG_EXE = os.path.join(FFMPEG_DIR, "ffmpeg.exe") + +def ensure_ffmpeg(): + if shutil.which("ffmpeg"): + return None + if os.path.exists(FFMPEG_EXE): + return FFMPEG_DIR + os.makedirs(FFMPEG_DIR, exist_ok=True) + zip_path = os.path.join(FFMPEG_DIR, "ffmpeg.zip") + url = "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip" + print("Скачиваю ffmpeg (один раз)...") + urllib.request.urlretrieve(url, zip_path) + with zipfile.ZipFile(zip_path, "r") as zf: + for name in zf.namelist(): + bname = os.path.basename(name) + if bname in ("ffmpeg.exe", "ffprobe.exe"): + src = zf.extract(name, FFMPEG_DIR) + dst = os.path.join(FFMPEG_DIR, bname) + if src != dst: + shutil.move(src, dst) + os.remove(zip_path) + for item in os.listdir(FFMPEG_DIR): + p = os.path.join(FFMPEG_DIR, item) + if os.path.isdir(p): + shutil.rmtree(p) + print("ffmpeg готов.") + return FFMPEG_DIR + +try: + import ctypes + ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), 7) +except Exception: + pass + +R = "\033[0m" +B = "\033[1m" +CYN = "\033[96m" +GRN = "\033[92m" +YLW = "\033[93m" +GRY = "\033[90m" +WHT = "\033[97m" +MGT = "\033[95m" + +PAGE_SIZE = 8 +ARL_FILE = os.path.join(os.path.expanduser("~"), ".deezer_arl.txt") +OUT_DIR = os.path.join(os.path.expanduser("~"), "Downloads", "Deezer") +BROWSERS = ["chrome", "edge", "firefox", "chromium", "opera", "brave", "vivaldi"] + +os.makedirs(OUT_DIR, exist_ok=True) +ffmpeg_location = ensure_ffmpeg() + +def fmt_dur(s): + if not s: return "?:??" + m, s = divmod(int(s), 60) + return f"{m}:{s:02d}" + +def trunc(s, n): + return s if len(s) <= n else s[:n-1] + "…" + +def read_key(): + ch = msvcrt.getch() + if ch in (b'\x00', b'\xe0'): + return ('spec', msvcrt.getch()) + return ('char', ch) + +def draw(items, sel, query, page, total_pages): + os.system('cls') + W = 74 + header = f" {CYN}{B}Deezer{R} {GRY}▸{R} {WHT}«{query}»{R}" + nav = f" {GRY}↑↓ выбор ←→ страница {page+1}/{total_pages} Enter скачать Esc новый поиск{R}" + print(f"\n{CYN}{'▄'*W}{R}") + print(header) + print(nav) + print(f"{CYN}{'▀'*W}{R}\n") + for i, it in enumerate(items): + artist = trunc(it.get('artist',''), 22) + title = trunc(it.get('title',''), 30) + album = trunc(it.get('album',''), 18) + dur = it.get('dur','') + line = f"{artist:<22} {title:<30} {GRY}{album:<18} {dur}{R}" + if i == sel: + print(f" {GRN}{B}▶ {line}{R}") + else: + print(f" {GRY}{line}{R}") + print(f"\n{CYN}{'▄'*W}{R}\n") + +def tui_pick(pages, query): + page = 0 + sel = 0 + while True: + cur = pages[page] + draw(cur, sel, query, page, len(pages)) + kind, key = read_key() + if kind == 'spec': + if key == b'H': + sel = max(0, sel - 1) + elif key == b'P': + sel = min(len(cur)-1, sel + 1) + elif key == b'M' and page < len(pages)-1: + page += 1; sel = 0 + elif key == b'K' and page > 0: + page -= 1; sel = 0 + elif kind == 'char': + if key == b'\r': + return cur[sel] + elif key == b'\x1b': + return None + +def search_deezer(query, limit=20): + q = urllib.parse.quote(query) + r = requests.get(f"https://api.deezer.com/search?q={q}&limit={limit}", timeout=10) + raw = r.json().get("data", []) + items = [] + for t in raw: + items.append({ + 'id': t['id'], + 'artist': t['artist']['name'], + 'title': t['title'], + 'album': t.get('album',{}).get('title',''), + 'dur': fmt_dur(t.get('duration',0)), + }) + return [items[i:i+PAGE_SIZE] for i in range(0, len(items), PAGE_SIZE)] if items else [] + +def get_arl_from_browser(): + try: + import browser_cookie3 + except ImportError: + pip_install("browser-cookie3") + import browser_cookie3 + for br in BROWSERS: + try: + loader = getattr(browser_cookie3, br, None) + if not loader: continue + for c in loader(domain_name=".deezer.com"): + if c.name == "arl" and len(c.value) > 50: + return c.value + except Exception: + continue + return None + +def load_arl(): + if os.path.exists(ARL_FILE): + v = open(ARL_FILE).read().strip() + if len(v) > 50: return v + return None + +def save_arl(v): + open(ARL_FILE, "w").write(v) + +def login_flow(): + os.system('cls') + print(f"\n{CYN}{B} Deezer — Авторизация{R}\n") + print(" Открываю браузер на странице входа...") + webbrowser.open("https://www.deezer.com/login") + time.sleep(2) + input(f"\n {YLW}Войди в Deezer в браузере, затем нажми Enter...{R} ") + print(" Считываю куки автоматически...") + arl = get_arl_from_browser() + if arl: + save_arl(arl) + print(f" {GRN}Авторизация успешна!{R}") + time.sleep(1) + return arl + print(f"\n {YLW}Не удалось считать автоматически.{R}") + print(" F12 → Application → Cookies → deezer.com → скопируй 'arl'") + arl = input(" Вставь ARL вручную: ").strip() + if len(arl) > 50: + save_arl(arl) + return arl + return None + +def download(arl, url, is_track=True): + opts = { + "outtmpl": os.path.join(OUT_DIR, "%(uploader)s - %(title)s.%(ext)s"), + "format": "flac/bestaudio/best", + "postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": "flac"}], + "quiet": False, + "noplaylist": is_track, + "http_headers": {"Cookie": f"arl={arl}"}, + } + if ffmpeg_location: + opts["ffmpeg_location"] = ffmpeg_location + with yt_dlp.YoutubeDL(opts) as ydl: + ydl.download([url]) + +def is_deezer_url(s): + return bool(re.match(r"https?://(www\.)?deezer\.com/", s)) + +def main(): + os.system('cls') + print(f"\n{CYN}{B} ══════════════════════════════════════{R}") + print(f"{CYN}{B} Deezer Downloader → FLAC Lossless {R}") + print(f"{CYN}{B} ══════════════════════════════════════{R}") + print(f" {GRY}Папка: {OUT_DIR}{R}\n") + + arl = load_arl() + if arl: + print(f" {GRN}✓ Авторизация найдена{R}\n") + else: + arl = login_flow() + if not arl: + print("Выход."); return + + while True: + print(f"\n {GRY}Введи URL или поисковый запрос │ 'login' │ 'q' выход{R}") + query = input(f" {CYN}▸{R} ").strip() + + if not query: continue + if query.lower() == 'q': break + if query.lower() == 'login': + arl = login_flow() + continue + + if is_deezer_url(query): + os.system('cls') + print(f"\n {GRN}Скачиваю: {query}{R}\n") + try: + download(arl, query, "/track/" in query) + print(f"\n {GRN}Готово! → {OUT_DIR}{R}") + except Exception as e: + print(f"\n Ошибка: {e}") + input(f"\n {GRY}Enter чтобы продолжить...{R}") + continue + + print(f" {GRY}Ищу...{R}", end="\r") + try: + pages = search_deezer(query) + except Exception as e: + print(f" Ошибка поиска: {e}\n"); continue + + if not pages: + print(f" {YLW}Ничего не найдено.{R}\n"); continue + + track = tui_pick(pages, query) + if track is None: + continue + + url = f"https://www.deezer.com/track/{track['id']}" + os.system('cls') + print(f"\n {GRN}Скачиваю: {track['artist']} — {track['title']}{R}\n") + try: + download(arl, url, True) + print(f"\n {GRN}Готово! → {OUT_DIR}{R}") + except Exception as e: + print(f"\n Ошибка: {e}") + if "401" in str(e) or "403" in str(e): + print(f" {YLW}Попробуй обновить авторизацию → введи 'login'{R}") + input(f"\n {GRY}Enter чтобы продолжить...{R}") + +if __name__ == "__main__": + main() + input("\nНажми Enter для выхода...") diff --git a/soundcloud_dl.py b/soundcloud_dl.py new file mode 100644 index 0000000..90d7807 --- /dev/null +++ b/soundcloud_dl.py @@ -0,0 +1,207 @@ +import subprocess +import sys +import os +import re +import zipfile +import shutil +import urllib.request +import msvcrt + +os.system('') + +def pip_install(pkg): + subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "--upgrade", pkg], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +try: + import yt_dlp +except ImportError: + print("Устанавливаю yt-dlp...") + pip_install("yt-dlp") + import yt_dlp + +FFMPEG_DIR = os.path.join(os.path.expanduser("~"), ".local_ffmpeg") +FFMPEG_EXE = os.path.join(FFMPEG_DIR, "ffmpeg.exe") + +def ensure_ffmpeg(): + if shutil.which("ffmpeg"): + return None + if os.path.exists(FFMPEG_EXE): + return FFMPEG_DIR + os.makedirs(FFMPEG_DIR, exist_ok=True) + zip_path = os.path.join(FFMPEG_DIR, "ffmpeg.zip") + url = "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip" + print("Скачиваю ffmpeg (один раз)...") + urllib.request.urlretrieve(url, zip_path) + with zipfile.ZipFile(zip_path, "r") as zf: + for name in zf.namelist(): + bname = os.path.basename(name) + if bname in ("ffmpeg.exe", "ffprobe.exe"): + src = zf.extract(name, FFMPEG_DIR) + dst = os.path.join(FFMPEG_DIR, bname) + if src != dst: + shutil.move(src, dst) + os.remove(zip_path) + for item in os.listdir(FFMPEG_DIR): + p = os.path.join(FFMPEG_DIR, item) + if os.path.isdir(p): + shutil.rmtree(p) + print("ffmpeg готов.") + return FFMPEG_DIR + +try: + import ctypes + ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), 7) +except Exception: + pass + +R = "\033[0m" +B = "\033[1m" +CYN = "\033[96m" +GRN = "\033[92m" +YLW = "\033[93m" +GRY = "\033[90m" +WHT = "\033[97m" +ORG = "\033[38;5;208m" + +PAGE_SIZE = 8 +OUT_DIR = os.path.join(os.path.expanduser("~"), "Downloads", "SoundCloud") +os.makedirs(OUT_DIR, exist_ok=True) +ffmpeg_location = ensure_ffmpeg() + +def fmt_dur(s): + if not s: return "?:??" + m, s = divmod(int(s), 60) + return f"{m}:{s:02d}" + +def trunc(s, n): + s = s or "" + return s if len(s) <= n else s[:n-1] + "…" + +def read_key(): + ch = msvcrt.getch() + if ch in (b'\x00', b'\xe0'): + return ('spec', msvcrt.getch()) + return ('char', ch) + +def draw(items, sel, query, page, total_pages): + os.system('cls') + W = 74 + print(f"\n{ORG}{'▄'*W}{R}") + print(f" {ORG}{B}SoundCloud{R} {GRY}▸{R} {WHT}«{query}»{R}") + print(f" {GRY}↑↓ выбор ←→ страница {page+1}/{total_pages} Enter скачать Esc новый поиск{R}") + print(f"{ORG}{'▀'*W}{R}\n") + for i, it in enumerate(items): + uploader = trunc(it.get('uploader',''), 24) + title = trunc(it.get('title',''), 32) + dur = it.get('dur','') + line = f"{uploader:<24} {title:<32} {GRY}{dur}{R}" + if i == sel: + print(f" {GRN}{B}▶ {line}{R}") + else: + print(f" {GRY}{line}{R}") + print(f"\n{ORG}{'▄'*W}{R}\n") + +def tui_pick(pages, query): + page = 0 + sel = 0 + while True: + cur = pages[page] + draw(cur, sel, query, page, len(pages)) + kind, key = read_key() + if kind == 'spec': + if key == b'H': + sel = max(0, sel - 1) + elif key == b'P': + sel = min(len(cur)-1, sel + 1) + elif key == b'M' and page < len(pages)-1: + page += 1; sel = 0 + elif key == b'K' and page > 0: + page -= 1; sel = 0 + elif kind == 'char': + if key == b'\r': + return cur[sel] + elif key == b'\x1b': + return None + +def search_soundcloud(query, limit=16): + opts = {"quiet": True, "extract_flat": True, "skip_download": True} + with yt_dlp.YoutubeDL(opts) as ydl: + info = ydl.extract_info(f"scsearch{limit}:{query}", download=False) + raw = info.get("entries", []) if info else [] + items = [] + for t in raw: + items.append({ + 'url': t.get('url') or t.get('webpage_url',''), + 'uploader': t.get('uploader',''), + 'title': t.get('title',''), + 'dur': fmt_dur(t.get('duration')), + }) + return [items[i:i+PAGE_SIZE] for i in range(0, len(items), PAGE_SIZE)] if items else [] + +def is_sc_url(s): + return bool(re.match(r"https?://(www\.)?soundcloud\.com/", s)) + +def download(url): + opts = { + "format": "bestaudio/best", + "outtmpl": os.path.join(OUT_DIR, "%(uploader)s - %(title)s.%(ext)s"), + "postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": "flac"}], + "quiet": False, + } + if ffmpeg_location: + opts["ffmpeg_location"] = ffmpeg_location + with yt_dlp.YoutubeDL(opts) as ydl: + ydl.download([url]) + +def main(): + os.system('cls') + print(f"\n{ORG}{B} ══════════════════════════════════════{R}") + print(f"{ORG}{B} SoundCloud Downloader → FLAC {R}") + print(f"{ORG}{B} ══════════════════════════════════════{R}") + print(f" {GRY}Папка: {OUT_DIR}{R}\n") + + while True: + print(f"\n {GRY}Введи URL или поисковый запрос │ 'q' выход{R}") + query = input(f" {ORG}▸{R} ").strip() + + if not query: continue + if query.lower() == 'q': break + + if is_sc_url(query): + os.system('cls') + print(f"\n {GRN}Скачиваю: {query}{R}\n") + try: + download(query) + print(f"\n {GRN}Готово! → {OUT_DIR}{R}") + except Exception as e: + print(f"\n Ошибка: {e}") + input(f"\n {GRY}Enter чтобы продолжить...{R}") + continue + + print(f" {GRY}Ищу...{R}", end="\r") + try: + pages = search_soundcloud(query) + except Exception as e: + print(f" Ошибка поиска: {e}\n"); continue + + if not pages: + print(f" {YLW}Ничего не найдено.{R}\n"); continue + + track = tui_pick(pages, query) + if track is None: + continue + + url = track['url'] + os.system('cls') + print(f"\n {GRN}Скачиваю: {track['uploader']} — {track['title']}{R}\n") + try: + download(url) + print(f"\n {GRN}Готово! → {OUT_DIR}{R}") + except Exception as e: + print(f"\n Ошибка: {e}") + input(f"\n {GRY}Enter чтобы продолжить...{R}") + +if __name__ == "__main__": + main() + input("\nНажми Enter для выхода...") diff --git a/start.py b/start.py new file mode 100644 index 0000000..1982094 --- /dev/null +++ b/start.py @@ -0,0 +1,112 @@ +# made by im.savsis.xyz with ♥ +import os +import sys +import subprocess +import msvcrt + +os.system('') + +try: + import ctypes + ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), 7) +except Exception: + pass + +R = "\033[0m" +B = "\033[1m" +GRN = "\033[92m" +GRY = "\033[90m" +WHT = "\033[97m" +CYN = "\033[96m" +RED = "\033[91m" +ORG = "\033[38;5;208m" +MGT = "\033[95m" +DIM = "\033[2m" + +BASE = os.path.dirname(os.path.abspath(__file__)) + +MENU = [ + { + "label": "Deezer", + "sub": "FLAC Lossless │ поиск + URL", + "icon": "◈", + "color": MGT, + "script": "deezer_dl.py", + }, + { + "label": "SoundCloud", + "sub": "FLAC │ поиск + URL", + "icon": "◈", + "color": ORG, + "script": "soundcloud_dl.py", + }, + { + "label": "YouTube", + "sub": "MP4 (макс. качество) │ поиск + URL", + "icon": "◈", + "color": RED, + "script": "youtube_dl.py", + }, +] + +def draw(sel): + os.system('cls') + W = 60 + print() + print(f" {WHT}{B}{'━'*W}{R}") + print(f" {WHT}{B}{'':^{W}}{R}") + title = "MUSIC DOWNLOADER PACK" + print(f" {WHT}{B}{title:^{W}}{R}") + brand = "im.savsis.xyz" + print(f" {GRY}{'made by ' + brand:^{W}}{R}") + print(f" {WHT}{B}{'':^{W}}{R}") + sub = "↑↓ выбор Enter запустить Q выход" + print(f" {GRY}{sub:^{W}}{R}") + print(f" {WHT}{B}{'':^{W}}{R}") + print(f" {WHT}{B}{'━'*W}{R}") + print() + + for i, item in enumerate(MENU): + c = item["color"] + lbl = item["label"] + sub = item["sub"] + if i == sel: + print(f" {c}{B} ▶ {lbl:<14}{R} {WHT}{sub}{R}") + else: + print(f" {GRY} {lbl:<14} {sub}{R}") + print() + + print(f" {WHT}{B}{'━'*W}{R}") + print(f"\n {DIM}{GRY}Файлы скачиваются в ~/Downloads/<сервис>{R}") + print(f" {DIM}{GRY}made by im.savsis.xyz ♥{R}\n") + +def read_key(): + ch = msvcrt.getch() + if ch in (b'\x00', b'\xe0'): + return ('spec', msvcrt.getch()) + return ('char', ch) + +def run_script(name): + path = os.path.join(BASE, name) + os.system('cls') + subprocess.run([sys.executable, path]) + +def main(): + sel = 0 + while True: + draw(sel) + kind, key = read_key() + if kind == 'spec': + if key == b'H': + sel = (sel - 1) % len(MENU) + elif key == b'P': + sel = (sel + 1) % len(MENU) + elif kind == 'char': + if key == b'\r': + run_script(MENU[sel]["script"]) + elif key in (b'q', b'Q', b'\x1b'): + os.system('cls') + break + +if __name__ == "__main__": + main() diff --git a/youtube_dl.py b/youtube_dl.py new file mode 100644 index 0000000..27a4108 --- /dev/null +++ b/youtube_dl.py @@ -0,0 +1,217 @@ +import subprocess +import sys +import os +import re +import zipfile +import shutil +import urllib.request +import msvcrt + +os.system('') + +def pip_install(pkg): + subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "--upgrade", pkg], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +try: + import yt_dlp +except ImportError: + print("Устанавливаю yt-dlp...") + pip_install("yt-dlp") + import yt_dlp + +FFMPEG_DIR = os.path.join(os.path.expanduser("~"), ".local_ffmpeg") +FFMPEG_EXE = os.path.join(FFMPEG_DIR, "ffmpeg.exe") + +def ensure_ffmpeg(): + if shutil.which("ffmpeg"): + return None + if os.path.exists(FFMPEG_EXE): + return FFMPEG_DIR + os.makedirs(FFMPEG_DIR, exist_ok=True) + zip_path = os.path.join(FFMPEG_DIR, "ffmpeg.zip") + url = "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip" + print("Скачиваю ffmpeg (один раз)...") + urllib.request.urlretrieve(url, zip_path) + with zipfile.ZipFile(zip_path, "r") as zf: + for name in zf.namelist(): + bname = os.path.basename(name) + if bname in ("ffmpeg.exe", "ffprobe.exe"): + src = zf.extract(name, FFMPEG_DIR) + dst = os.path.join(FFMPEG_DIR, bname) + if src != dst: + shutil.move(src, dst) + os.remove(zip_path) + for item in os.listdir(FFMPEG_DIR): + p = os.path.join(FFMPEG_DIR, item) + if os.path.isdir(p): + shutil.rmtree(p) + print("ffmpeg готов.") + return FFMPEG_DIR + +try: + import ctypes + ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), 7) +except Exception: + pass + +R = "\033[0m" +B = "\033[1m" +RED = "\033[91m" +GRN = "\033[92m" +YLW = "\033[93m" +GRY = "\033[90m" +WHT = "\033[97m" + +PAGE_SIZE = 8 +OUT_DIR = os.path.join(os.path.expanduser("~"), "Downloads", "YouTube") +os.makedirs(OUT_DIR, exist_ok=True) +ffmpeg_location = ensure_ffmpeg() + +def fmt_dur(s): + if not s: return "?:??" + m, s = divmod(int(s), 60) + h, m = divmod(m, 60) + if h: return f"{h}:{m:02d}:{s:02d}" + return f"{m}:{s:02d}" + +def fmt_views(v): + if not v: return "" + if v >= 1_000_000_000: return f"{v/1e9:.1f}B" + if v >= 1_000_000: return f"{v/1e6:.1f}M" + if v >= 1_000: return f"{v/1e3:.0f}K" + return str(v) + +def trunc(s, n): + s = s or "" + return s if len(s) <= n else s[:n-1] + "…" + +def read_key(): + ch = msvcrt.getch() + if ch in (b'\x00', b'\xe0'): + return ('spec', msvcrt.getch()) + return ('char', ch) + +def draw(items, sel, query, page, total_pages): + os.system('cls') + W = 74 + print(f"\n{RED}{'▄'*W}{R}") + print(f" {RED}{B}YouTube{R} {GRY}▸{R} {WHT}«{query}»{R}") + print(f" {GRY}↑↓ выбор ←→ страница {page+1}/{total_pages} Enter скачать Esc новый поиск{R}") + print(f"{RED}{'▀'*W}{R}\n") + for i, it in enumerate(items): + channel = trunc(it.get('channel',''), 22) + title = trunc(it.get('title',''), 30) + dur = it.get('dur','') + views = it.get('views','') + line = f"{channel:<22} {title:<30} {GRY}{dur:<7} {views}{R}" + if i == sel: + print(f" {GRN}{B}▶ {line}{R}") + else: + print(f" {GRY}{line}{R}") + print(f"\n{RED}{'▄'*W}{R}\n") + +def tui_pick(pages, query): + page = 0 + sel = 0 + while True: + cur = pages[page] + draw(cur, sel, query, page, len(pages)) + kind, key = read_key() + if kind == 'spec': + if key == b'H': + sel = max(0, sel - 1) + elif key == b'P': + sel = min(len(cur)-1, sel + 1) + elif key == b'M' and page < len(pages)-1: + page += 1; sel = 0 + elif key == b'K' and page > 0: + page -= 1; sel = 0 + elif kind == 'char': + if key == b'\r': + return cur[sel] + elif key == b'\x1b': + return None + +def search_youtube(query, limit=16): + opts = {"quiet": True, "extract_flat": True, "skip_download": True} + with yt_dlp.YoutubeDL(opts) as ydl: + info = ydl.extract_info(f"ytsearch{limit}:{query}", download=False) + raw = info.get("entries", []) if info else [] + items = [] + for v in raw: + vid = v.get('id','') + items.append({ + 'url': v.get('url') or v.get('webpage_url') or f"https://www.youtube.com/watch?v={vid}", + 'channel': v.get('channel') or v.get('uploader',''), + 'title': v.get('title',''), + 'dur': fmt_dur(v.get('duration')), + 'views': fmt_views(v.get('view_count')), + }) + return [items[i:i+PAGE_SIZE] for i in range(0, len(items), PAGE_SIZE)] if items else [] + +def is_yt_url(s): + return bool(re.match(r"https?://(www\.)?(youtube\.com|youtu\.be)/", s)) + +def download(url): + opts = { + "format": "bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio/best", + "outtmpl": os.path.join(OUT_DIR, "%(channel)s - %(title)s.%(ext)s"), + "merge_output_format": "mp4", + "quiet": False, + } + if ffmpeg_location: + opts["ffmpeg_location"] = ffmpeg_location + with yt_dlp.YoutubeDL(opts) as ydl: + ydl.download([url]) + +def main(): + os.system('cls') + print(f"\n{RED}{B} ══════════════════════════════════════════════{R}") + print(f"{RED}{B} YouTube Downloader → MP4 (макс. качество) {R}") + print(f"{RED}{B} ══════════════════════════════════════════════{R}") + print(f" {GRY}Папка: {OUT_DIR}{R}\n") + + while True: + print(f"\n {GRY}Введи URL или поисковый запрос │ 'q' выход{R}") + query = input(f" {RED}▸{R} ").strip() + + if not query: continue + if query.lower() == 'q': break + + if is_yt_url(query): + os.system('cls') + print(f"\n {GRN}Скачиваю: {query}{R}\n") + try: + download(query) + print(f"\n {GRN}Готово! → {OUT_DIR}{R}") + except Exception as e: + print(f"\n Ошибка: {e}") + input(f"\n {GRY}Enter чтобы продолжить...{R}") + continue + + print(f" {GRY}Ищу...{R}", end="\r") + try: + pages = search_youtube(query) + except Exception as e: + print(f" Ошибка поиска: {e}\n"); continue + + if not pages: + print(f" {YLW}Ничего не найдено.{R}\n"); continue + + video = tui_pick(pages, query) + if video is None: + continue + + os.system('cls') + print(f"\n {GRN}Скачиваю: {video['title']}{R}\n") + try: + download(video['url']) + print(f"\n {GRN}Готово! → {OUT_DIR}{R}") + except Exception as e: + print(f"\n Ошибка: {e}") + input(f"\n {GRY}Enter чтобы продолжить...{R}") + +if __name__ == "__main__": + main() + input("\nНажми Enter для выхода...")