update
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user