This commit is contained in:
Marek
2026-04-04 23:04:10 +02:00
parent 823adb707d
commit d789a31642
15 changed files with 232 additions and 0 deletions

20
backend/database.py Normal file
View 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()

View File

@@ -1,7 +1,25 @@
from fastapi import FastAPI 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 = 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("/") @app.get("/")
def root(): def root():

17
backend/models.py Normal file
View 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)

View File

@@ -1,3 +1,6 @@
fastapi fastapi
uvicorn uvicorn
yt-dlp yt-dlp
sqlalchemy
aiosqlite
python-multipart

View File

Binary file not shown.

Binary file not shown.

81
backend/routes/videos.py Normal file
View 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
View 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,
)

View File

Binary file not shown.

View 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()

View 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()