Erfan commited on
Commit
49bbcb9
·
1 Parent(s): 24cd3e4

fix: template engine, youtube cookies, ffmpeg

Browse files
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
- return {
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, Request, Form
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, log_stat
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(request: Request):
22
- return templates.TemplateResponse("index.html", {"request": request})
23
 
24
 
25
  @router.get("/youtube", response_class=HTMLResponse)
26
- async def youtube_page(request: Request):
27
- return templates.TemplateResponse("youtube.html", {"request": request})
28
 
29
 
30
  @router.post("/youtube", response_class=HTMLResponse)
31
- async def youtube_download(request: Request, url: str = Form(...), format: str = Form("audio")):
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 templates.TemplateResponse("youtube.html", {
40
- "request": request,
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 templates.TemplateResponse("youtube.html", {
49
- "request": request,
50
- "error": str(e),
51
- })
52
 
53
 
54
  @router.get("/filedl", response_class=HTMLResponse)
55
- async def filedl_page(request: Request):
56
- return templates.TemplateResponse("filedl.html", {"request": request})
57
 
58
 
59
  @router.post("/filedl", response_class=HTMLResponse)
60
- async def filedl_download(request: Request, url: str = Form(...)):
61
  try:
62
  filepath, error, content_type, size = await download_file(url)
63
  if error:
64
- return templates.TemplateResponse("filedl.html", {"request": request, "error": error})
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 templates.TemplateResponse("filedl.html", {
69
- "request": request,
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 templates.TemplateResponse("filedl.html", {"request": request, "error": str(e)})
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(request: Request):
87
- return templates.TemplateResponse("chat.html", {"request": request, "messages": []})
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(request: Request):
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 templates.TemplateResponse("admin/dashboard.html", {
111
- "request": request,
112
- "total_users": summary[0] if summary else 0,
113
- "total_downloads": total_downloads,
114
- "chatgpt_count": chatgpt_count,
115
- "users": users,
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(request: Request):
128
  settings = {
129
  "openai_key": await get_setting("openai_key", ""),
130
  "webhook_url": await get_setting("webhook_url", ""),
131
  }
132
- return templates.TemplateResponse("admin/settings.html", {"request": request, "settings": settings})
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 templates.TemplateResponse("admin/settings.html", {
146
- "request": request,
147
- "settings": {"openai_key": openai_key, "webhook_url": webhook_url},
148
- "saved": True,
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
- <form method="POST" action="/admin/settings" class="space-y-4">
 
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 %}