update
This commit is contained in:
20
backend/database.py
Normal file
20
backend/database.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
|
||||
DATABASE_URL = "sqlite:///videos/youtubeapp.db"
|
||||
|
||||
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def create_tables():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
@@ -1,7 +1,25 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from database import create_tables
|
||||
from routes.videos import router as videos_router
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(videos_router)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def startup():
|
||||
create_tables()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def root():
|
||||
|
||||
17
backend/models.py
Normal file
17
backend/models.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, DateTime, Integer, String
|
||||
|
||||
from database import Base
|
||||
|
||||
|
||||
class Video(Base):
|
||||
__tablename__ = "videos"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
title = Column(String, nullable=False)
|
||||
youtuber = Column(String, nullable=False)
|
||||
thumbnail_url = Column(String, nullable=False)
|
||||
youtube_url = Column(String, nullable=False, unique=True)
|
||||
file_path = Column(String, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
@@ -1,3 +1,6 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
yt-dlp
|
||||
sqlalchemy
|
||||
aiosqlite
|
||||
python-multipart
|
||||
|
||||
0
backend/routes/__init__.py
Normal file
0
backend/routes/__init__.py
Normal file
BIN
backend/routes/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/routes/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/routes/__pycache__/videos.cpython-312.pyc
Normal file
BIN
backend/routes/__pycache__/videos.cpython-312.pyc
Normal file
Binary file not shown.
81
backend/routes/videos.py
Normal file
81
backend/routes/videos.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from database import get_db
|
||||
from schemas import VideoCreate, VideoResponse
|
||||
from services import video_service
|
||||
from services.download_service import download_video
|
||||
|
||||
router = APIRouter(prefix="/videos", tags=["videos"])
|
||||
|
||||
|
||||
@router.post("", response_model=VideoResponse)
|
||||
def create_video(video_data: VideoCreate, db: Session = Depends(get_db)):
|
||||
video = video_service.create_video(db, video_data)
|
||||
return VideoResponse.from_model(video)
|
||||
|
||||
|
||||
@router.get("", response_model=list[VideoResponse])
|
||||
def get_all_videos(db: Session = Depends(get_db)):
|
||||
videos = video_service.get_all_videos(db)
|
||||
return [VideoResponse.from_model(v) for v in videos]
|
||||
|
||||
|
||||
@router.get("/downloaded", response_model=list[VideoResponse])
|
||||
def get_downloaded_videos(db: Session = Depends(get_db)):
|
||||
videos = video_service.get_downloaded_videos(db)
|
||||
return [VideoResponse.from_model(v) for v in videos]
|
||||
|
||||
|
||||
@router.post("/{video_id}/download")
|
||||
def trigger_download(video_id: int, db: Session = Depends(get_db)):
|
||||
video = video_service.get_video(db, video_id)
|
||||
if not video:
|
||||
raise HTTPException(status_code=404, detail="Video nicht gefunden")
|
||||
if video.file_path:
|
||||
return {"status": "already_downloaded"}
|
||||
|
||||
thread = threading.Thread(target=download_video, args=(video.id, video.youtube_url))
|
||||
thread.start()
|
||||
return {"status": "download_started"}
|
||||
|
||||
|
||||
@router.get("/{video_id}/stream")
|
||||
def stream_video(video_id: int, db: Session = Depends(get_db)):
|
||||
video = video_service.get_video(db, video_id)
|
||||
if not video:
|
||||
raise HTTPException(status_code=404, detail="Video nicht gefunden")
|
||||
|
||||
if not video.file_path:
|
||||
download_video(video.id, video.youtube_url)
|
||||
db.refresh(video)
|
||||
|
||||
path = Path(video.file_path)
|
||||
if not path.exists():
|
||||
raise HTTPException(status_code=404, detail="Videodatei nicht gefunden")
|
||||
|
||||
def iter_file():
|
||||
with open(path, "rb") as f:
|
||||
while chunk := f.read(1024 * 1024):
|
||||
yield chunk
|
||||
|
||||
return StreamingResponse(iter_file(), media_type="video/mp4")
|
||||
|
||||
|
||||
@router.get("/{video_id}/file")
|
||||
def download_file(video_id: int, db: Session = Depends(get_db)):
|
||||
video = video_service.get_video(db, video_id)
|
||||
if not video:
|
||||
raise HTTPException(status_code=404, detail="Video nicht gefunden")
|
||||
if not video.file_path:
|
||||
raise HTTPException(status_code=404, detail="Video noch nicht heruntergeladen")
|
||||
|
||||
path = Path(video.file_path)
|
||||
if not path.exists():
|
||||
raise HTTPException(status_code=404, detail="Videodatei nicht gefunden")
|
||||
|
||||
return FileResponse(path, media_type="video/mp4", filename=f"{video.title}.mp4")
|
||||
35
backend/schemas.py
Normal file
35
backend/schemas.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class VideoCreate(BaseModel):
|
||||
title: str
|
||||
youtuber: str
|
||||
thumbnail_url: str
|
||||
youtube_url: str
|
||||
|
||||
|
||||
class VideoResponse(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
youtuber: str
|
||||
thumbnail_url: str
|
||||
youtube_url: str
|
||||
is_downloaded: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, video):
|
||||
return cls(
|
||||
id=video.id,
|
||||
title=video.title,
|
||||
youtuber=video.youtuber,
|
||||
thumbnail_url=video.thumbnail_url,
|
||||
youtube_url=video.youtube_url,
|
||||
is_downloaded=video.file_path is not None,
|
||||
created_at=video.created_at,
|
||||
)
|
||||
0
backend/services/__init__.py
Normal file
0
backend/services/__init__.py
Normal file
BIN
backend/services/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/services/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/services/__pycache__/download_service.cpython-312.pyc
Normal file
BIN
backend/services/__pycache__/download_service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/services/__pycache__/video_service.cpython-312.pyc
Normal file
BIN
backend/services/__pycache__/video_service.cpython-312.pyc
Normal file
Binary file not shown.
27
backend/services/download_service.py
Normal file
27
backend/services/download_service.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import subprocess
|
||||
|
||||
from database import SessionLocal
|
||||
from services.video_service import get_video, update_file_path
|
||||
|
||||
VIDEOS_DIR = "/videos"
|
||||
|
||||
|
||||
def download_video(video_id: int, youtube_url: str):
|
||||
output_path = f"{VIDEOS_DIR}/{video_id}.mp4"
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"yt-dlp",
|
||||
"-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]",
|
||||
"-o", output_path,
|
||||
"--merge-output-format", "mp4",
|
||||
youtube_url,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
update_file_path(db, video_id, output_path)
|
||||
finally:
|
||||
db.close()
|
||||
31
backend/services/video_service.py
Normal file
31
backend/services/video_service.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models import Video
|
||||
from schemas import VideoCreate
|
||||
|
||||
|
||||
def create_video(db: Session, video_data: VideoCreate) -> Video:
|
||||
video = Video(**video_data.model_dump())
|
||||
db.add(video)
|
||||
db.commit()
|
||||
db.refresh(video)
|
||||
return video
|
||||
|
||||
|
||||
def get_all_videos(db: Session) -> list[Video]:
|
||||
return db.query(Video).order_by(Video.created_at.desc()).all()
|
||||
|
||||
|
||||
def get_downloaded_videos(db: Session) -> list[Video]:
|
||||
return db.query(Video).filter(Video.file_path.isnot(None)).order_by(Video.created_at.desc()).all()
|
||||
|
||||
|
||||
def get_video(db: Session, video_id: int) -> Video | None:
|
||||
return db.query(Video).filter(Video.id == video_id).first()
|
||||
|
||||
|
||||
def update_file_path(db: Session, video_id: int, path: str):
|
||||
video = get_video(db, video_id)
|
||||
if video:
|
||||
video.file_path = path
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user