File size: 6,003 Bytes
65d3b67 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
from fastapi import FastAPI, HTTPException, Request, Query, Response
from fastapi.responses import StreamingResponse, HTMLResponse
from fastapi.templating import Jinja2Templates
from pytubefix import YouTube
from pytubefix.cli import on_progress
import os
import logging
import httpx
import hashlib
from functools import lru_cache
from encrypt import encrypt_video_id, decrypt_video_id
app = FastAPI()
CHUNK_SIZE = 1024 * 1024 # 1MB
logger = logging.getLogger(__name__)
def open_file_range(file_path: str, start: int, end: int):
with open(file_path, "rb") as f:
f.seek(start)
bytes_to_read = end - start + 1
while bytes_to_read > 0:
chunk = f.read(min(CHUNK_SIZE, bytes_to_read))
if not chunk:
break
bytes_to_read -= len(chunk)
yield chunk
def generate_etag(file_path):
hasher = hashlib.md5()
with open(file_path, 'rb') as f:
while chunk := f.read(8192):
hasher.update(chunk)
return hasher.hexdigest()
@lru_cache(maxsize=128)
def get_video_metadata(video_id: str):
yt = YouTube(f"https://www.youtube.com/watch?v={video_id}", client='WEB_EMBED')
if yt.length >= 600:
return {
"title": yt.title,
"description": yt.description,
"author": yt.author,
"duration": yt.length,
"views": yt.views,
"date": yt.publish_date,
"video_url": yt.streams.get_highest_resolution().url,
"audio_url": yt.streams.get_audio_only().url,
}
else:
return {
"title": yt.title,
"description": yt.description,
"author": yt.author,
"duration": yt.length,
"views": yt.views,
"date": yt.publish_date,
}
@app.get("/api/video/{video_id}")
def get_video_info(video_id: str, request: Request):
try:
metadata = get_video_metadata(video_id)
encrypted_video_id = encrypt_video_id(video_id)
BASE_URL = request.base_url
if metadata['duration'] >= 600:
return {**metadata}
else:
return {
**metadata,
"video_url": f"{BASE_URL}video/{encrypted_video_id}",
"audio_url": f"{BASE_URL}audio/{encrypted_video_id}"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@app.get("/video/{video_id}")
async def stream_video(video_id: str, request: Request, download: bool = Query(False)):
try:
decrypted_video_id = decrypt_video_id(video_id)
yt = YouTube(f"https://www.youtube.com/watch?v={decrypted_video_id}")
stream = yt.streams.get_highest_resolution()
url = stream.url
headers = {}
if range_header := request.headers.get("range"):
headers["Range"] = range_header
async def proxy_stream():
try:
async with httpx.AsyncClient() as client:
async with client.stream("GET", url, headers=headers, timeout=60) as response:
if response.status_code not in (200, 206):
logger.error(f"Failed to stream: {response.status_code}")
return
async for chunk in response.aiter_bytes(CHUNK_SIZE):
yield chunk
except Exception as e:
logger.error(f"Streaming error: {str(e)}")
return
response_headers = {
"Accept-Ranges": "bytes",
"Cache-Control": "public, max-age=3600"
}
# Handle filename safely
title = yt.title.encode("utf-8", "ignore").decode("utf-8")
if download:
response_headers["Content-Disposition"] = f'attachment; filename="{title}.mp4"'
else:
response_headers["Content-Disposition"] = f'inline; filename="{title}.mp4"'
return StreamingResponse(
proxy_stream(),
media_type="video/mp4",
headers=response_headers
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Could not fetch video URL: {str(e)}")
@app.get("/audio/{video_id}")
async def stream_audio(video_id: str, request: Request, download: bool = Query(False)):
try:
decrypted_video_id = decrypt_video_id(video_id)
yt = YouTube(f"https://www.youtube.com/watch?v={decrypted_video_id}")
stream = yt.streams.get_audio_only()
url = stream.url
headers = {
"User-Agent": request.headers.get("user-agent", "Mozilla/5.0"),
}
if range_header := request.headers.get("range"):
headers["Range"] = range_header
async def proxy_stream():
async with httpx.AsyncClient(follow_redirects=True) as client:
async with client.stream("GET", url, headers=headers) as response:
if response.status_code not in (200, 206):
raise HTTPException(status_code=502, detail="Source stream error")
async for chunk in response.aiter_bytes(CHUNK_SIZE):
yield chunk
response_headers = {
"Accept-Ranges": "bytes",
"Cache-Control": "public, max-age=3600"
}
# Handle filename safely
title = yt.title.encode("utf-8", "ignore").decode("utf-8")
if download:
response_headers["Content-Disposition"] = f'attachment; filename="{title}.mp3"'
else:
response_headers["Content-Disposition"] = f'inline; filename="{title}.mp3"'
return StreamingResponse(
proxy_stream(),
media_type=stream.mime_type or "audio/mp4",
headers=response_headers
)
except Exception as e:
logger.error(f"Streaming error: {e}")
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|