1
0
Fork 0

fix readme content

This commit is contained in:
Samive 2026-04-01 14:23:38 +08:00
commit 32cce9a0ae
2 changed files with 4 additions and 372 deletions

View file

@ -25,13 +25,8 @@ A beautiful, self-hosted web application for downloading YouTube (and 1000+ othe
### Pull and run from Docker Hub
```bash
<<<<<<< HEAD
docker pull samive/yt-dlp-web:latest
docker run -d -p 8000:8000 --name yt-dlp-web samive/yt-dlp-web:latest
=======
docker pull YOUR_DOCKERHUB_USERNAME/yt-dlp-web:latest
docker run -d -p 8000:8000 --name yt-dlp-web YOUR_DOCKERHUB_USERNAME/yt-dlp-web:latest
>>>>>>> 831b5f7dab502c2631355cc9a518dcd5d832690b
```
Then open **http://localhost:8000** in your browser.
@ -43,11 +38,7 @@ docker run -d \
-p 8000:8000 \
-v $(pwd)/downloads:/app/web/backend/downloads \
--name yt-dlp-web \
<<<<<<< HEAD
samive/yt-dlp-web:latest
=======
YOUR_DOCKERHUB_USERNAME/yt-dlp-web:latest
>>>>>>> 831b5f7dab502c2631355cc9a518dcd5d832690b
```
### Stop / remove the container
@ -68,7 +59,7 @@ docker rm yt-dlp-web
### Build the image
```bash
git clone https://github.com/YOUR_GITHUB_USERNAME/yt-dlp-web.git
git clone https://github.com/samive/yt-dlp-web.git
cd yt-dlp-web
docker build -t yt-dlp-web .
```
@ -93,11 +84,8 @@ Open **http://localhost:8000**.
### Setup
```bash
# Install yt-dlp from source
pip install -e ".[default]"
# Install backend dependencies
pip install fastapi "uvicorn[standard]" httpx
# Install dependencies
pip install yt-dlp fastapi "uvicorn[standard]" httpx
# Start the server
python -m uvicorn web.backend.main:app --reload --port 8000
@ -109,7 +97,7 @@ Open **http://localhost:8000**.
## Usage Guide
1. **Paste a URL** — Paste any YouTube (or supported site) link into the input field and click **Fetch**
1. **Paste a URL** — Paste any YouTube or supported site link into the input field and click **Fetch**
2. **Review video info** — Thumbnail, title, uploader, duration, views, likes, and upload date are shown
3. **Select a format** — Choose from the dropdown (highest quality is pre-selected)
4. **Download** — Click the **Download** button and watch the live progress bar
@ -124,73 +112,13 @@ docker run -d \
-p 8000:8000 \
-v $(pwd)/cookies.txt:/app/web/backend/cookies.txt:ro \
--name yt-dlp-web \
<<<<<<< HEAD
samive/yt-dlp-web:latest
=======
YOUR_DOCKERHUB_USERNAME/yt-dlp-web:latest
>>>>>>> 831b5f7dab502c2631355cc9a518dcd5d832690b
```
> **Note:** Using cookies with Chrome on Windows may cause issues due to DPAPI encryption. Firefox or Edge cookies work more reliably.
---
## Publish to Docker Hub
### 1. Create a Docker Hub account
Sign up at [hub.docker.com](https://hub.docker.com) and create a public repository named `yt-dlp-web`.
### 2. Log in from terminal
```bash
docker login
```
### 3. Build and tag the image
```bash
<<<<<<< HEAD
docker build -t samive/yt-dlp-web:latest .
=======
docker build -t YOUR_DOCKERHUB_USERNAME/yt-dlp-web:latest .
>>>>>>> 831b5f7dab502c2631355cc9a518dcd5d832690b
```
### 4. Push to Docker Hub
```bash
<<<<<<< HEAD
docker push samive/yt-dlp-web:latest
=======
docker push YOUR_DOCKERHUB_USERNAME/yt-dlp-web:latest
>>>>>>> 831b5f7dab502c2631355cc9a518dcd5d832690b
```
### 5. (Optional) Tag a version
```bash
<<<<<<< HEAD
docker tag samive/yt-dlp-web:latest samive/yt-dlp-web:1.0.0
docker push samive/yt-dlp-web:1.0.0
=======
docker tag YOUR_DOCKERHUB_USERNAME/yt-dlp-web:latest YOUR_DOCKERHUB_USERNAME/yt-dlp-web:1.0.0
docker push YOUR_DOCKERHUB_USERNAME/yt-dlp-web:1.0.0
>>>>>>> 831b5f7dab502c2631355cc9a518dcd5d832690b
```
Anyone can now pull and run your image with:
```bash
<<<<<<< HEAD
docker pull samive/yt-dlp-web:latest
=======
docker pull YOUR_DOCKERHUB_USERNAME/yt-dlp-web:latest
>>>>>>> 831b5f7dab502c2631355cc9a518dcd5d832690b
```
---
## Tech Stack
- **Backend** — Python, [FastAPI](https://fastapi.tiangolo.com/), [yt-dlp](https://github.com/yt-dlp/yt-dlp)

View file

@ -1,296 +0,0 @@
<<<<<<< HEAD
=======
import sys
>>>>>>> 831b5f7dab502c2631355cc9a518dcd5d832690b
import json
import uuid
import asyncio
import httpx
import urllib.parse
from pathlib import Path
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, Response, JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
<<<<<<< HEAD
=======
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
>>>>>>> 831b5f7dab502c2631355cc9a518dcd5d832690b
import yt_dlp
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
return JSONResponse(status_code=500, content={"detail": str(exc)})
DOWNLOAD_DIR = Path(__file__).parent / "downloads"
DOWNLOAD_DIR.mkdir(exist_ok=True)
FRONTEND_DIR = Path(__file__).parent.parent / "frontend"
FORMAT_PRESETS = {
"best": "bestvideo+bestaudio/bestvideo/best",
"1080p": "bestvideo[height<=1080]+bestaudio/bestvideo[height<=1080]/best",
"720p": "bestvideo[height<=720]+bestaudio/bestvideo[height<=720]/best",
"480p": "bestvideo[height<=480]+bestaudio/bestvideo[height<=480]/best",
"audio": "bestaudio/best",
}
# file_id -> { status, percent, eta, speed, filepath, ... }
_progress: dict[str, dict] = {}
# file_id -> asyncio.Task
_tasks: dict[str, asyncio.Task] = {}
class InfoRequest(BaseModel):
url: str
class DownloadRequest(BaseModel):
url: str
preset: str = "best"
format_id: str | None = None
file_id: str | None = None
@app.get("/")
async def index():
return FileResponse(FRONTEND_DIR / "index.html")
@app.post("/info")
async def get_info(req: InfoRequest):
ydl_opts = {"quiet": True, "no_warnings": True, "skip_download": True}
info = None
try:
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = await asyncio.to_thread(ydl.extract_info, req.url, download=False)
except Exception:
pass
if not info:
try:
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = await asyncio.to_thread(ydl.extract_info, req.url, download=False)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
if not info:
raise HTTPException(status_code=400, detail="Could not extract video info.")
thumbnails = info.get("thumbnails") or []
thumbnail_url = None
if thumbnails:
ranked = sorted(
(t for t in thumbnails if t.get("url")),
key=lambda t: (t.get("width") or 0) * (t.get("height") or 0),
reverse=True,
)
thumbnail_url = ranked[0]["url"] if ranked else None
thumbnail_url = thumbnail_url or info.get("thumbnail")
formats = [
{
"format_id": f.get("format_id"),
"ext": f.get("ext"),
"resolution": f.get("resolution") or f.get("format_note") or "unknown",
"filesize": f.get("filesize") or f.get("filesize_approx"),
"vcodec": f.get("vcodec"),
"acodec": f.get("acodec"),
"fps": f.get("fps"),
"height": f.get("height") or 0,
"tbr": f.get("tbr") or 0,
}
for f in (info.get("formats") or [])
if f.get("ext") != "mhtml"
and f.get("protocol") != "mhtml"
and (f.get("vcodec", "none") != "none" or f.get("acodec", "none") != "none")
]
formats.sort(key=lambda f: (f["height"], f["tbr"]), reverse=True)
if not formats:
raise HTTPException(status_code=400, detail="No downloadable formats found.")
return {
"title": info.get("title"),
"thumbnail": f"/thumbnail?url={thumbnail_url}" if thumbnail_url else None,
"duration": info.get("duration"),
"uploader": info.get("uploader"),
"view_count": info.get("view_count"),
"like_count": info.get("like_count"),
"upload_date": info.get("upload_date"),
"formats": formats,
}
@app.get("/thumbnail")
async def proxy_thumbnail(url: str):
try:
async with httpx.AsyncClient(follow_redirects=True, timeout=10) as client:
r = await client.get(url, headers={"User-Agent": "Mozilla/5.0"})
return Response(content=r.content, media_type=r.headers.get("content-type", "image/jpeg"))
except Exception:
raise HTTPException(status_code=502, detail="Could not fetch thumbnail")
async def _run_download(file_id: str, url: str, fmt: str, output_template: str):
"""Runs yt-dlp in a thread and updates _progress. Cancellable via task cancellation."""
cancelled = asyncio.Event()
def progress_hook(d):
if cancelled.is_set():
raise yt_dlp.utils.DownloadError("Cancelled")
status = d.get("status")
if status == "downloading":
downloaded = d.get("downloaded_bytes", 0)
total = d.get("total_bytes") or d.get("total_bytes_estimate") or 0
pct = round((downloaded / total) * 100, 1) if total else 0
eta = d.get("eta")
speed = d.get("speed")
_progress[file_id].update({
"status": "downloading",
"percent": pct,
"eta": int(eta) if eta else None,
"speed": round(speed / 1024 / 1024, 2) if speed else None,
})
elif status == "finished":
_progress[file_id].update({"status": "finished", "percent": 100, "eta": 0})
ydl_opts = {
"quiet": True,
"no_warnings": True,
"format": fmt,
"outtmpl": output_template,
"merge_output_format": "mp4",
"progress_hooks": [progress_hook],
}
try:
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = await asyncio.to_thread(ydl.extract_info, url, download=True)
return info
except asyncio.CancelledError:
cancelled.set()
_progress[file_id]["status"] = "cancelled"
raise
except Exception as e:
_progress[file_id]["status"] = "error"
_progress[file_id]["error"] = str(e)
raise
@app.post("/download")
async def download_video(req: DownloadRequest):
file_id = req.file_id or str(uuid.uuid4())
output_template = str(DOWNLOAD_DIR / f"{file_id}.%(ext)s")
fmt = req.format_id if req.format_id else FORMAT_PRESETS.get(req.preset, FORMAT_PRESETS["best"])
_progress[file_id] = {
"status": "starting", "percent": 0, "eta": None, "speed": None,
"url": req.url, "fmt": fmt, "output_template": output_template,
}
task = asyncio.create_task(_run_download(file_id, req.url, fmt, output_template))
_tasks[file_id] = task
try:
info = await task
except asyncio.CancelledError:
return JSONResponse({"file_id": file_id, "status": "cancelled"})
except Exception as e:
_tasks.pop(file_id, None)
raise HTTPException(status_code=400, detail=_progress.get(file_id, {}).get("error", str(e)))
_tasks.pop(file_id, None)
matches = list(DOWNLOAD_DIR.glob(f"{file_id}.*"))
if not matches:
raise HTTPException(status_code=500, detail="Download failed: file not found")
filepath = matches[0]
title = info.get("title") or file_id
filename_ascii = "".join(c for c in title if c.isascii() and (c.isalnum() or c in " ._-()")).strip()
if not filename_ascii:
filename_ascii = file_id
filename_ascii += filepath.suffix
filename_encoded = urllib.parse.quote(title + filepath.suffix)
_progress[file_id].update({
"status": "ready",
"percent": 100,
"filepath": str(filepath),
"filename_ascii": filename_ascii,
"filename_encoded": filename_encoded,
})
return JSONResponse({"file_id": file_id, "status": "ready"})
@app.post("/cancel/{file_id}")
async def cancel_download(file_id: str):
task = _tasks.get(file_id)
if task and not task.done():
task.cancel()
# Clean up partial files
for f in DOWNLOAD_DIR.glob(f"{file_id}*"):
f.unlink(missing_ok=True)
_progress[file_id] = {"status": "cancelled", "percent": 0}
return JSONResponse({"status": "cancelled"})
return JSONResponse({"status": "not_found"})
@app.get("/file/{file_id}")
async def serve_file(file_id: str):
meta = _progress.get(file_id)
if not meta or meta.get("status") != "ready":
raise HTTPException(status_code=404, detail="File not ready or already downloaded")
filepath = Path(meta["filepath"])
if not filepath.exists():
raise HTTPException(status_code=404, detail="File not found")
filename_ascii = meta["filename_ascii"]
filename_encoded = meta["filename_encoded"]
def iterfile():
with open(filepath, "rb") as f:
yield from f
filepath.unlink(missing_ok=True)
_progress.pop(file_id, None)
return StreamingResponse(
iterfile(),
media_type="video/mp4",
headers={
"Content-Disposition": f"attachment; filename=\"{filename_ascii}\"; filename*=UTF-8''{filename_encoded}",
},
)
@app.get("/progress/{file_id}")
async def progress_stream(file_id: str, request: Request):
async def event_generator():
while True:
if await request.is_disconnected():
break
data = _progress.get(file_id)
if data:
yield f"data: {json.dumps(data)}\n\n"
if data.get("status") in ("ready", "cancelled", "error"):
break
await asyncio.sleep(0.5)
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static")