Erfan commited on
Commit ·
49bbcb9
1
Parent(s): 24cd3e4
fix: template engine, youtube cookies, ffmpeg
Browse files- Aptfile +1 -0
- bot/youtube.py +15 -2
- web/routes.py +49 -55
- web/template_engine.py +9 -0
- web/templates/admin/settings.html +16 -6
Aptfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
ffmpeg
|
bot/youtube.py
CHANGED
|
@@ -5,18 +5,29 @@ from config import DOWNLOAD_DIR
|
|
| 5 |
|
| 6 |
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
def _get_ydl_opts(format_spec: str, ext: str):
|
| 10 |
-
|
| 11 |
"format": format_spec,
|
| 12 |
"outtmpl": os.path.join(DOWNLOAD_DIR, f"{uuid.uuid4()}.%(ext)s"),
|
| 13 |
"quiet": True,
|
| 14 |
"no_warnings": True,
|
| 15 |
}
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
async def get_video_info(url: str):
|
| 19 |
ydl_opts = {"quiet": True, "no_warnings": True}
|
|
|
|
| 20 |
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
| 21 |
info = ydl.extract_info(url, download=False)
|
| 22 |
return {
|
|
@@ -42,6 +53,8 @@ async def download_yt(url: str, quality: str = "audio"):
|
|
| 42 |
else:
|
| 43 |
opts = _get_ydl_opts("best", "mp4")
|
| 44 |
|
|
|
|
|
|
|
| 45 |
with yt_dlp.YoutubeDL(opts) as ydl:
|
| 46 |
info = ydl.extract_info(url, download=True)
|
| 47 |
file_path = ydl.prepare_filename(info)
|
|
@@ -54,6 +67,6 @@ async def download_yt(url: str, quality: str = "audio"):
|
|
| 54 |
return file_path, info.get("title", "video")
|
| 55 |
for f in os.listdir(DOWNLOAD_DIR):
|
| 56 |
p = os.path.join(DOWNLOAD_DIR, f)
|
| 57 |
-
if os.path.isfile(p):
|
| 58 |
return p, info.get("title", "video")
|
| 59 |
return None, None
|
|
|
|
| 5 |
|
| 6 |
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
|
| 7 |
|
| 8 |
+
COOKIES_FILE = os.path.join(DOWNLOAD_DIR, "..", "cookies.txt")
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _cookies_opts():
|
| 12 |
+
if os.path.exists(COOKIES_FILE):
|
| 13 |
+
return {"cookiefile": COOKIES_FILE}
|
| 14 |
+
return {}
|
| 15 |
+
|
| 16 |
|
| 17 |
def _get_ydl_opts(format_spec: str, ext: str):
|
| 18 |
+
opts = {
|
| 19 |
"format": format_spec,
|
| 20 |
"outtmpl": os.path.join(DOWNLOAD_DIR, f"{uuid.uuid4()}.%(ext)s"),
|
| 21 |
"quiet": True,
|
| 22 |
"no_warnings": True,
|
| 23 |
}
|
| 24 |
+
opts.update(_cookies_opts())
|
| 25 |
+
return opts
|
| 26 |
|
| 27 |
|
| 28 |
async def get_video_info(url: str):
|
| 29 |
ydl_opts = {"quiet": True, "no_warnings": True}
|
| 30 |
+
ydl_opts.update(_cookies_opts())
|
| 31 |
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
| 32 |
info = ydl.extract_info(url, download=False)
|
| 33 |
return {
|
|
|
|
| 53 |
else:
|
| 54 |
opts = _get_ydl_opts("best", "mp4")
|
| 55 |
|
| 56 |
+
opts["paths"] = {"home": DOWNLOAD_DIR}
|
| 57 |
+
|
| 58 |
with yt_dlp.YoutubeDL(opts) as ydl:
|
| 59 |
info = ydl.extract_info(url, download=True)
|
| 60 |
file_path = ydl.prepare_filename(info)
|
|
|
|
| 67 |
return file_path, info.get("title", "video")
|
| 68 |
for f in os.listdir(DOWNLOAD_DIR):
|
| 69 |
p = os.path.join(DOWNLOAD_DIR, f)
|
| 70 |
+
if os.path.isfile(p) and f != "cookies.txt":
|
| 71 |
return p, info.get("title", "video")
|
| 72 |
return None, None
|
web/routes.py
CHANGED
|
@@ -1,77 +1,59 @@
|
|
| 1 |
-
from fastapi import APIRouter,
|
| 2 |
-
from fastapi.responses import HTMLResponse, JSONResponse
|
| 3 |
-
from fastapi.templating import Jinja2Templates
|
| 4 |
-
import os
|
| 5 |
-
|
| 6 |
-
from fastapi.responses import FileResponse
|
| 7 |
import os
|
| 8 |
|
| 9 |
from bot.youtube import get_video_info, download_yt
|
| 10 |
from bot.filedl import download_file, cleanup_file
|
| 11 |
from bot.chatgpt import ask_gpt
|
| 12 |
-
from db import get_all_users, get_stats_summary, set_admin, is_admin, get_setting, set_setting
|
| 13 |
from config import DOWNLOAD_DIR
|
| 14 |
-
|
| 15 |
-
templates = Jinja2Templates(directory="web/templates")
|
| 16 |
|
| 17 |
router = APIRouter()
|
| 18 |
|
| 19 |
|
| 20 |
@router.get("/", response_class=HTMLResponse)
|
| 21 |
-
async def index(
|
| 22 |
-
return
|
| 23 |
|
| 24 |
|
| 25 |
@router.get("/youtube", response_class=HTMLResponse)
|
| 26 |
-
async def youtube_page(
|
| 27 |
-
return
|
| 28 |
|
| 29 |
|
| 30 |
@router.post("/youtube", response_class=HTMLResponse)
|
| 31 |
-
async def youtube_download(
|
| 32 |
try:
|
| 33 |
info = await get_video_info(url)
|
| 34 |
quality = "audio" if format == "audio" else "720p"
|
| 35 |
filepath, title = await download_yt(url, quality)
|
| 36 |
if filepath and os.path.exists(filepath):
|
| 37 |
-
# Re-host the file for download
|
| 38 |
download_url = f"/download/{os.path.basename(filepath)}"
|
| 39 |
-
return
|
| 40 |
-
|
| 41 |
-
"result": {"title": title, "download_url": download_url},
|
| 42 |
-
})
|
| 43 |
-
return templates.TemplateResponse("youtube.html", {
|
| 44 |
-
"request": request,
|
| 45 |
-
"error": "Processing failed.",
|
| 46 |
-
})
|
| 47 |
except Exception as e:
|
| 48 |
-
return
|
| 49 |
-
"request": request,
|
| 50 |
-
"error": str(e),
|
| 51 |
-
})
|
| 52 |
|
| 53 |
|
| 54 |
@router.get("/filedl", response_class=HTMLResponse)
|
| 55 |
-
async def filedl_page(
|
| 56 |
-
return
|
| 57 |
|
| 58 |
|
| 59 |
@router.post("/filedl", response_class=HTMLResponse)
|
| 60 |
-
async def filedl_download(
|
| 61 |
try:
|
| 62 |
filepath, error, content_type, size = await download_file(url)
|
| 63 |
if error:
|
| 64 |
-
return
|
| 65 |
if filepath and os.path.exists(filepath):
|
| 66 |
sz = f"{size / 1024 / 1024:.1f} MB" if size > 1024 * 1024 else f"{size / 1024:.1f} KB"
|
| 67 |
download_url = f"/download/{os.path.basename(filepath)}"
|
| 68 |
-
return
|
| 69 |
-
|
| 70 |
-
"result": {"size": sz, "download_url": download_url},
|
| 71 |
-
})
|
| 72 |
-
return templates.TemplateResponse("filedl.html", {"request": request, "error": "Download failed."})
|
| 73 |
except Exception as e:
|
| 74 |
-
return
|
| 75 |
|
| 76 |
|
| 77 |
@router.get("/download/{filename}")
|
|
@@ -83,8 +65,8 @@ async def serve_file(filename: str):
|
|
| 83 |
|
| 84 |
|
| 85 |
@router.get("/chat", response_class=HTMLResponse)
|
| 86 |
-
async def chat_page(
|
| 87 |
-
return
|
| 88 |
|
| 89 |
|
| 90 |
@router.post("/chat")
|
|
@@ -102,18 +84,17 @@ async def chat_send(data: dict):
|
|
| 102 |
# --- Admin routes ---
|
| 103 |
|
| 104 |
@router.get("/admin", response_class=HTMLResponse)
|
| 105 |
-
async def admin_dashboard(
|
| 106 |
users = await get_all_users()
|
| 107 |
summary, stat_rows = await get_stats_summary()
|
| 108 |
total_downloads = sum(r["count"] for r in stat_rows if r["action"] in ("youtube", "filedl"))
|
| 109 |
chatgpt_count = sum(r["count"] for r in stat_rows if r["action"] == "chatgpt")
|
| 110 |
-
return
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
})
|
| 117 |
|
| 118 |
|
| 119 |
@router.post("/admin/toggle-admin")
|
|
@@ -124,17 +105,16 @@ async def toggle_admin(bale_id: str = Form(...)):
|
|
| 124 |
|
| 125 |
|
| 126 |
@router.get("/admin/settings", response_class=HTMLResponse)
|
| 127 |
-
async def admin_settings_page(
|
| 128 |
settings = {
|
| 129 |
"openai_key": await get_setting("openai_key", ""),
|
| 130 |
"webhook_url": await get_setting("webhook_url", ""),
|
| 131 |
}
|
| 132 |
-
return
|
| 133 |
|
| 134 |
|
| 135 |
@router.post("/admin/settings")
|
| 136 |
async def admin_settings_save(
|
| 137 |
-
request: Request,
|
| 138 |
openai_key: str = Form(""),
|
| 139 |
webhook_url: str = Form(""),
|
| 140 |
):
|
|
@@ -142,8 +122,22 @@ async def admin_settings_save(
|
|
| 142 |
await set_setting("webhook_url", webhook_url)
|
| 143 |
if openai_key:
|
| 144 |
os.environ["OPENAI_API_KEY"] = openai_key
|
| 145 |
-
return
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Form, UploadFile, File
|
| 2 |
+
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import os
|
| 4 |
|
| 5 |
from bot.youtube import get_video_info, download_yt
|
| 6 |
from bot.filedl import download_file, cleanup_file
|
| 7 |
from bot.chatgpt import ask_gpt
|
| 8 |
+
from db import get_all_users, get_stats_summary, set_admin, is_admin, get_setting, set_setting
|
| 9 |
from config import DOWNLOAD_DIR
|
| 10 |
+
from web.template_engine import render
|
|
|
|
| 11 |
|
| 12 |
router = APIRouter()
|
| 13 |
|
| 14 |
|
| 15 |
@router.get("/", response_class=HTMLResponse)
|
| 16 |
+
async def index():
|
| 17 |
+
return render("index.html")
|
| 18 |
|
| 19 |
|
| 20 |
@router.get("/youtube", response_class=HTMLResponse)
|
| 21 |
+
async def youtube_page():
|
| 22 |
+
return render("youtube.html")
|
| 23 |
|
| 24 |
|
| 25 |
@router.post("/youtube", response_class=HTMLResponse)
|
| 26 |
+
async def youtube_download(url: str = Form(...), format: str = Form("audio")):
|
| 27 |
try:
|
| 28 |
info = await get_video_info(url)
|
| 29 |
quality = "audio" if format == "audio" else "720p"
|
| 30 |
filepath, title = await download_yt(url, quality)
|
| 31 |
if filepath and os.path.exists(filepath):
|
|
|
|
| 32 |
download_url = f"/download/{os.path.basename(filepath)}"
|
| 33 |
+
return render("youtube.html", result={"title": title, "download_url": download_url})
|
| 34 |
+
return render("youtube.html", error="Processing failed.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
except Exception as e:
|
| 36 |
+
return render("youtube.html", error=str(e))
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
|
| 39 |
@router.get("/filedl", response_class=HTMLResponse)
|
| 40 |
+
async def filedl_page():
|
| 41 |
+
return render("filedl.html")
|
| 42 |
|
| 43 |
|
| 44 |
@router.post("/filedl", response_class=HTMLResponse)
|
| 45 |
+
async def filedl_download(url: str = Form(...)):
|
| 46 |
try:
|
| 47 |
filepath, error, content_type, size = await download_file(url)
|
| 48 |
if error:
|
| 49 |
+
return render("filedl.html", error=error)
|
| 50 |
if filepath and os.path.exists(filepath):
|
| 51 |
sz = f"{size / 1024 / 1024:.1f} MB" if size > 1024 * 1024 else f"{size / 1024:.1f} KB"
|
| 52 |
download_url = f"/download/{os.path.basename(filepath)}"
|
| 53 |
+
return render("filedl.html", result={"size": sz, "download_url": download_url})
|
| 54 |
+
return render("filedl.html", error="Download failed.")
|
|
|
|
|
|
|
|
|
|
| 55 |
except Exception as e:
|
| 56 |
+
return render("filedl.html", error=str(e))
|
| 57 |
|
| 58 |
|
| 59 |
@router.get("/download/{filename}")
|
|
|
|
| 65 |
|
| 66 |
|
| 67 |
@router.get("/chat", response_class=HTMLResponse)
|
| 68 |
+
async def chat_page():
|
| 69 |
+
return render("chat.html", messages=[])
|
| 70 |
|
| 71 |
|
| 72 |
@router.post("/chat")
|
|
|
|
| 84 |
# --- Admin routes ---
|
| 85 |
|
| 86 |
@router.get("/admin", response_class=HTMLResponse)
|
| 87 |
+
async def admin_dashboard():
|
| 88 |
users = await get_all_users()
|
| 89 |
summary, stat_rows = await get_stats_summary()
|
| 90 |
total_downloads = sum(r["count"] for r in stat_rows if r["action"] in ("youtube", "filedl"))
|
| 91 |
chatgpt_count = sum(r["count"] for r in stat_rows if r["action"] == "chatgpt")
|
| 92 |
+
return render("admin/dashboard.html",
|
| 93 |
+
total_users=summary[0] if summary else 0,
|
| 94 |
+
total_downloads=total_downloads,
|
| 95 |
+
chatgpt_count=chatgpt_count,
|
| 96 |
+
users=users,
|
| 97 |
+
)
|
|
|
|
| 98 |
|
| 99 |
|
| 100 |
@router.post("/admin/toggle-admin")
|
|
|
|
| 105 |
|
| 106 |
|
| 107 |
@router.get("/admin/settings", response_class=HTMLResponse)
|
| 108 |
+
async def admin_settings_page():
|
| 109 |
settings = {
|
| 110 |
"openai_key": await get_setting("openai_key", ""),
|
| 111 |
"webhook_url": await get_setting("webhook_url", ""),
|
| 112 |
}
|
| 113 |
+
return render("admin/settings.html", settings=settings)
|
| 114 |
|
| 115 |
|
| 116 |
@router.post("/admin/settings")
|
| 117 |
async def admin_settings_save(
|
|
|
|
| 118 |
openai_key: str = Form(""),
|
| 119 |
webhook_url: str = Form(""),
|
| 120 |
):
|
|
|
|
| 122 |
await set_setting("webhook_url", webhook_url)
|
| 123 |
if openai_key:
|
| 124 |
os.environ["OPENAI_API_KEY"] = openai_key
|
| 125 |
+
return render("admin/settings.html", settings={"openai_key": openai_key, "webhook_url": webhook_url}, saved=True)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
@router.post("/admin/upload-cookies")
|
| 129 |
+
async def upload_cookies(cookies: UploadFile = File(...)):
|
| 130 |
+
from bot.youtube import COOKIES_FILE
|
| 131 |
+
try:
|
| 132 |
+
content = await cookies.read()
|
| 133 |
+
with open(COOKIES_FILE, "wb") as f:
|
| 134 |
+
f.write(content)
|
| 135 |
+
return render("admin/settings.html",
|
| 136 |
+
settings={"openai_key": await get_setting("openai_key", ""), "webhook_url": await get_setting("webhook_url", "")},
|
| 137 |
+
cookies_status={"success": True, "message": f"Cookies uploaded ({len(content)} bytes)"},
|
| 138 |
+
)
|
| 139 |
+
except Exception as e:
|
| 140 |
+
return render("admin/settings.html",
|
| 141 |
+
settings={"openai_key": await get_setting("openai_key", ""), "webhook_url": await get_setting("webhook_url", "")},
|
| 142 |
+
cookies_status={"success": False, "message": str(e)},
|
| 143 |
+
)
|
web/template_engine.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from jinja2 import Environment, FileSystemLoader
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
_env = Environment(loader=FileSystemLoader(Path(__file__).parent / "templates"), autoescape=True)
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def render(name: str, **context):
|
| 8 |
+
t = _env.get_template(name)
|
| 9 |
+
return t.render(**context)
|
web/templates/admin/settings.html
CHANGED
|
@@ -3,19 +3,29 @@
|
|
| 3 |
{% block content %}
|
| 4 |
<div class="max-w-2xl mx-auto py-8">
|
| 5 |
<h1 class="text-3xl font-bold mb-6">⚙️ Bot Settings</h1>
|
| 6 |
-
|
|
|
|
| 7 |
<div>
|
| 8 |
<label class="block text-gray-400 mb-1">OpenAI API Key</label>
|
| 9 |
<input type="password" name="openai_key" value="{{ settings.openai_key or '' }}"
|
| 10 |
class="w-full p-3 rounded bg-gray-800 border border-gray-700 focus:border-purple-500 outline-none">
|
| 11 |
</div>
|
| 12 |
-
<div>
|
| 13 |
-
<label class="block text-gray-400 mb-1">Webhook URL</label>
|
| 14 |
-
<input type="url" name="webhook_url" value="{{ settings.webhook_url or '' }}"
|
| 15 |
-
class="w-full p-3 rounded bg-gray-800 border border-gray-700 focus:border-purple-500 outline-none">
|
| 16 |
-
</div>
|
| 17 |
<button type="submit" class="p-3 bg-purple-600 rounded hover:bg-purple-500 font-semibold">Save Settings</button>
|
| 18 |
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
{% if saved %}
|
| 20 |
<div class="mt-4 p-3 bg-green-900/50 border border-green-700 rounded text-green-300">Settings saved!</div>
|
| 21 |
{% endif %}
|
|
|
|
| 3 |
{% block content %}
|
| 4 |
<div class="max-w-2xl mx-auto py-8">
|
| 5 |
<h1 class="text-3xl font-bold mb-6">⚙️ Bot Settings</h1>
|
| 6 |
+
|
| 7 |
+
<form method="POST" action="/admin/settings" class="space-y-4 mb-8">
|
| 8 |
<div>
|
| 9 |
<label class="block text-gray-400 mb-1">OpenAI API Key</label>
|
| 10 |
<input type="password" name="openai_key" value="{{ settings.openai_key or '' }}"
|
| 11 |
class="w-full p-3 rounded bg-gray-800 border border-gray-700 focus:border-purple-500 outline-none">
|
| 12 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
<button type="submit" class="p-3 bg-purple-600 rounded hover:bg-purple-500 font-semibold">Save Settings</button>
|
| 14 |
</form>
|
| 15 |
+
|
| 16 |
+
<h2 class="text-xl font-bold mb-4">🍪 YouTube Cookies</h2>
|
| 17 |
+
<p class="text-gray-400 text-sm mb-4">Upload a <code>cookies.txt</code> file to bypass YouTube's bot detection. <a href="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp" class="text-purple-400" target="_blank">How to get cookies</a></p>
|
| 18 |
+
<form method="POST" action="/admin/upload-cookies" enctype="multipart/form-data" class="space-y-4">
|
| 19 |
+
<input type="file" name="cookies" accept=".txt"
|
| 20 |
+
class="w-full p-3 rounded bg-gray-800 border border-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:bg-purple-600 file:text-white hover:file:bg-purple-500">
|
| 21 |
+
<button type="submit" class="p-3 bg-purple-600 rounded hover:bg-purple-500 font-semibold">Upload Cookies</button>
|
| 22 |
+
</form>
|
| 23 |
+
{% if cookies_status %}
|
| 24 |
+
<div class="mt-4 p-3 rounded {{ 'bg-green-900/50 border border-green-700 text-green-300' if cookies_status.success else 'bg-red-900/50 border border-red-700 text-red-300' }}">
|
| 25 |
+
{{ cookies_status.message }}
|
| 26 |
+
</div>
|
| 27 |
+
{% endif %}
|
| 28 |
+
|
| 29 |
{% if saved %}
|
| 30 |
<div class="mt-4 p-3 bg-green-900/50 border border-green-700 rounded text-green-300">Settings saved!</div>
|
| 31 |
{% endif %}
|