Add files via upload
This commit is contained in:
parent
e3c927b1e8
commit
e1c52f2f7c
4 changed files with 821 additions and 0 deletions
285
deezer_dl.py
Normal file
285
deezer_dl.py
Normal 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
207
soundcloud_dl.py
Normal 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
112
start.py
Normal 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
217
youtube_dl.py
Normal 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 для выхода...")
|
||||
Loading…
Add table
Add a link
Reference in a new issue