Add files via upload

This commit is contained in:
Savsis 2026-07-25 10:37:57 +05:00 • committed by GitHub
parent e3c927b1e8
commit e1c52f2f7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 821 additions and 0 deletions

285
deezer_dl.py Normal file
View file

@ -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 для выхода...")

207
soundcloud_dl.py Normal file
View file

@ -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 для выхода...")

112
start.py Normal file
View file

@ -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()

217
youtube_dl.py Normal file
View file

@ -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 для выхода...")