This commit is contained in:
Marek Lenczewski
2026-04-07 18:01:34 +02:00
parent ca988345e9
commit 375a9cd386
5 changed files with 73 additions and 65 deletions

View File

@@ -20,18 +20,6 @@ class VideoResponse(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@classmethod
def fromModel(cls, video):
return cls(
id=video.id,
title=video.title,
youtuber=video.youtuber,
thumbnailUrl=video.thumbnailUrl,
youtubeUrl=video.youtubeUrl,
isDownloaded=video.filePath is not None,
profileIds=[p.id for p in video.profiles],
)
class CleanupRequest(BaseModel): class CleanupRequest(BaseModel):
profileId: int profileId: int

View File

@@ -1,4 +1,3 @@
import threading
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -7,11 +6,11 @@ from fastapi.responses import FileResponse, StreamingResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from api.schemas import CleanupRequest, VideoCreate, VideoResponse from api.schemas import CleanupRequest, VideoCreate, VideoResponse
from database.database import SessionLocal, getDb from database.database import getDb
from download.download_service import downloadVideo from download.download_service import downloadAsync
from model.video import Video from model.video import Video
from notify.notify_clients import notifyClients from notify.notify_clients import notifyClients
from stream.stream_service import streamVideoLive from stream.stream_service import streamAndSave
router = APIRouter(prefix="/videos", tags=["videos"]) router = APIRouter(prefix="/videos", tags=["videos"])
@@ -30,26 +29,21 @@ async def create(videoData: VideoCreate, db: Session = Depends(getDb)):
@router.get("", response_model=list[VideoResponse]) @router.get("", response_model=list[VideoResponse])
def getAll( def getAll(profileId: Optional[int] = Query(None), db: Session = Depends(getDb)):
profileId: Optional[int] = Query(None), return Video.getAll(db, profileId=profileId)
db: Session = Depends(getDb),
):
videos = Video.getAll(db, profileId=profileId)
return [VideoResponse.fromModel(v) for v in videos]
@router.get("/downloaded", response_model=list[VideoResponse]) @router.get("/downloaded", response_model=list[VideoResponse])
def getDownloaded( def getDownloaded(profileId: Optional[int] = Query(None), db: Session = Depends(getDb)):
profileId: Optional[int] = Query(None), return Video.getDownloaded(db, profileId=profileId)
db: Session = Depends(getDb),
):
videos = Video.getDownloaded(db, profileId=profileId)
return [VideoResponse.fromModel(v) for v in videos]
@router.post("/cleanup") @router.post("/cleanup")
def cleanup(request: CleanupRequest, db: Session = Depends(getDb)): def cleanup(request: CleanupRequest, db: Session = Depends(getDb)):
count = Video.deleteNotDownloaded(db, request.profileId, request.excludeIds or None) profileId = request.profileId
excludeIds = request.excludeIds
count = Video.deleteNotDownloaded(db, profileId, excludeIds)
return {"deleted": count} return {"deleted": count}
@@ -57,12 +51,11 @@ def cleanup(request: CleanupRequest, db: Session = Depends(getDb)):
def download(videoId: int, db: Session = Depends(getDb)): def download(videoId: int, db: Session = Depends(getDb)):
video = Video.getById(db, videoId) video = Video.getById(db, videoId)
if not video: if not video:
raise HTTPException(status_code=404, detail="Video nicht gefunden") raise HTTPException(404, "Video nicht gefunden")
if video.filePath: if video.filePath:
return {"status": "already_downloaded"} return {"status": "already_downloaded"}
thread = threading.Thread(target=downloadVideo, args=(video.id, video.youtubeUrl)) downloadAsync(videoId, video.youtubeUrl)
thread.start()
return {"status": "download_started"} return {"status": "download_started"}
@@ -70,52 +63,27 @@ def download(videoId: int, db: Session = Depends(getDb)):
def stream(videoId: int, db: Session = Depends(getDb)): def stream(videoId: int, db: Session = Depends(getDb)):
video = Video.getById(db, videoId) video = Video.getById(db, videoId)
if not video: if not video:
raise HTTPException(status_code=404, detail="Video nicht gefunden") raise HTTPException(404, "Video nicht gefunden")
if not video.filePath: if not video.filePath:
def streamAndSave(): return StreamingResponse(streamAndSave(videoId, video.youtubeUrl), media_type="video/mp4")
outputPath = f"/videos/{videoId}.mp4"
yield from streamVideoLive(videoId, video.youtubeUrl)
if Path(outputPath).exists():
sdb = SessionLocal()
try:
Video.updateFilePath(sdb, videoId, outputPath)
finally:
sdb.close()
return StreamingResponse(streamAndSave(), media_type="video/mp4")
path = Path(video.filePath) path = Path(video.filePath)
if not path.exists(): if not path.exists():
raise HTTPException(status_code=404, detail="Videodatei nicht gefunden") raise HTTPException(404, "Videodatei nicht gefunden")
return FileResponse(path, media_type="video/mp4") return FileResponse(path, media_type="video/mp4")
@router.get("/{videoId}/file") @router.get("/{videoId}/file")
def getFile(videoId: int, db: Session = Depends(getDb)): def getFile(videoId: int, db: Session = Depends(getDb)):
video = Video.getById(db, videoId) path, video = Video.getValidPath(db, videoId)
if not video: if not path:
raise HTTPException(status_code=404, detail="Video nicht gefunden") raise HTTPException(404, "Video noch nicht heruntergeladen")
if not video.filePath:
raise HTTPException(status_code=404, detail="Video noch nicht heruntergeladen")
path = Path(video.filePath)
if not path.exists():
Video.updateFilePath(db, videoId, None)
raise HTTPException(status_code=404, detail="Video noch nicht heruntergeladen")
return FileResponse(path, media_type="video/mp4", filename=f"{video.title}.mp4") return FileResponse(path, media_type="video/mp4", filename=f"{video.title}.mp4")
@router.delete("/{videoId}/file") @router.delete("/{videoId}/file")
def deleteFile(videoId: int, db: Session = Depends(getDb)): def deleteFile(videoId: int, db: Session = Depends(getDb)):
video = Video.getById(db, videoId) if not Video.deleteServerFile(db, videoId):
if not video: raise HTTPException(404, "Video nicht gefunden")
raise HTTPException(status_code=404, detail="Video nicht gefunden")
if video.filePath:
path = Path(video.filePath)
if path.exists():
path.unlink()
Video.updateFilePath(db, videoId, None)
return {"status": "deleted"} return {"status": "deleted"}

View File

@@ -1,4 +1,5 @@
import subprocess import subprocess
import threading
from pathlib import Path from pathlib import Path
from database.database import SessionLocal from database.database import SessionLocal
@@ -8,6 +9,10 @@ VIDEOS_DIR = "/videos"
MIN_VALID_SIZE = 1024 * 100 # 100 KB MIN_VALID_SIZE = 1024 * 100 # 100 KB
def downloadAsync(videoId: int, youtubeUrl: str):
threading.Thread(target=downloadVideo, args=(videoId, youtubeUrl)).start()
def downloadVideo(videoId: int, youtubeUrl: str): def downloadVideo(videoId: int, youtubeUrl: str):
outputPath = f"{VIDEOS_DIR}/{videoId}.mp4" outputPath = f"{VIDEOS_DIR}/{videoId}.mp4"

View File

@@ -1,3 +1,5 @@
from pathlib import Path
from sqlalchemy import Column, Integer, String from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import Session, relationship from sqlalchemy.orm import Session, relationship
@@ -17,6 +19,14 @@ class Video(Base):
filePath = Column("file_path", String, nullable=True) filePath = Column("file_path", String, nullable=True)
profiles = relationship("Profile", secondary=videoProfiles, backref="videos") profiles = relationship("Profile", secondary=videoProfiles, backref="videos")
@property
def isDownloaded(self) -> bool:
return self.filePath is not None
@property
def profileIds(self) -> list[int]:
return [p.id for p in self.profiles]
@classmethod @classmethod
def deleteIfExists(cls, db: Session, youtubeUrl: str, profileId: int | None): def deleteIfExists(cls, db: Session, youtubeUrl: str, profileId: int | None):
if not profileId: if not profileId:
@@ -80,6 +90,29 @@ class Video(Base):
video.filePath = path video.filePath = path
db.commit() db.commit()
@classmethod
def getValidPath(cls, db: Session, videoId: int):
video = cls.getById(db, videoId)
if not video or not video.filePath:
return None, None
path = Path(video.filePath)
if not path.exists():
cls.updateFilePath(db, videoId, None)
return None, None
return path, video
@classmethod
def deleteServerFile(cls, db: Session, videoId: int) -> bool:
video = cls.getById(db, videoId)
if not video:
return False
if video.filePath:
path = Path(video.filePath)
if path.exists():
path.unlink()
cls.updateFilePath(db, videoId, None)
return True
@classmethod @classmethod
def deleteNotDownloaded(cls, db: Session, profileId: int, excludeIds: list[int] | None = None) -> int: def deleteNotDownloaded(cls, db: Session, profileId: int, excludeIds: list[int] | None = None) -> int:
query = db.query(cls).filter( query = db.query(cls).filter(

View File

@@ -1,10 +1,24 @@
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from database.database import SessionLocal
VIDEOS_DIR = "/videos" VIDEOS_DIR = "/videos"
CHUNK_SIZE = 64 * 1024 CHUNK_SIZE = 64 * 1024
def streamAndSave(videoId: int, youtubeUrl: str):
from model.video import Video # Lazy-Import gegen Zirkular
outputPath = f"{VIDEOS_DIR}/{videoId}.mp4"
yield from streamVideoLive(videoId, youtubeUrl)
if Path(outputPath).exists():
db = SessionLocal()
try:
Video.updateFilePath(db, videoId, outputPath)
finally:
db.close()
def _getStreamUrls(youtubeUrl: str): def _getStreamUrls(youtubeUrl: str):
result = subprocess.run( result = subprocess.run(
[ [