From dc5f07fd78cf2a1504e9ba386ae60314c4a71a41 Mon Sep 17 00:00:00 2001 From: Viner Abubakirov Date: Wed, 25 Feb 2026 12:11:06 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=AD=D0=9F=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B9=20=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=81=D0=B8=D0=B8=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B2=D0=B8=D0=B4=D0=B5=D0=BE=20=D1=81=20YouTube?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v2/endpoints/youtube.py | 22 ++++++++++++++++++++ app/api/v2/router.py | 8 +++++++ app/main.py | 3 +++ app/schemas.py | 4 ++++ app/services.py | 37 +++++++++++++++++++++++++++++++-- app/tasks.py | 15 +++++++++++-- app/utils/uploader.py | 14 +++++++------ app/utils/youtubeV2.py | 12 +++++------ 8 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 app/api/v2/endpoints/youtube.py create mode 100644 app/api/v2/router.py diff --git a/app/api/v2/endpoints/youtube.py b/app/api/v2/endpoints/youtube.py new file mode 100644 index 0000000..7f44a38 --- /dev/null +++ b/app/api/v2/endpoints/youtube.py @@ -0,0 +1,22 @@ +from fastapi import APIRouter +from app.schemas import TaskCreateResponse +from app.schemas import DownloadRequest +from app.tasks import download_youtube +from app.services import YouTubeService + +router = APIRouter() + + +@router.post("/download", response_model=TaskCreateResponse) +async def download_video(data: DownloadRequest): + task = download_youtube.delay(url=str(data.url), quality=data.quality) + return TaskCreateResponse(task_id=task.id, status=task.status) + + +@router.get("/resolutions") +async def video_resolutions(url: str): + return {"resolutions": YouTubeService.resolutions(url)} + +@router.get("/size") +async def video_size(data: DownloadRequest): + return {"size": YouTubeService.filesize(data)} diff --git a/app/api/v2/router.py b/app/api/v2/router.py new file mode 100644 index 0000000..4498ae8 --- /dev/null +++ b/app/api/v2/router.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter +from app.api.v2.endpoints import youtube + + +router = APIRouter() + + +router.include_router(youtube.router, prefix="/youtube", tags=["YouTube"]) diff --git a/app/main.py b/app/main.py index 11098cd..45cb100 100644 --- a/app/main.py +++ b/app/main.py @@ -1,9 +1,12 @@ from fastapi import FastAPI from app.api.v1.router import router as v1_router +from app.api.v2.router import router as v2_router + app = FastAPI() app.include_router(v1_router, prefix="/api/v1") +app.include_router(v2_router, prefix="/api/v2") @app.get("/ping") diff --git a/app/schemas.py b/app/schemas.py index 86d6a34..71ac438 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -11,6 +11,10 @@ class DownloadResponse(BaseModel): audio: str +class DownloadResponseV2(BaseModel): + video: str + + class TaskCreateResponse(BaseModel): task_id: str status: str diff --git a/app/services.py b/app/services.py index b8eba71..30dc848 100644 --- a/app/services.py +++ b/app/services.py @@ -2,19 +2,52 @@ import os from app.core.config import settings from app.core.uploader import uploader_backend -from app.utils.youtube import YtDlpManager -from app.schemas import DownloadRequest, DownloadResponse +from app.utils.uploader import S3UploadBackend +from app.schemas import DownloadRequest, DownloadResponse, DownloadResponseV2 class YouTubeService: @staticmethod def download(data: DownloadRequest): + from app.utils.youtube import YtDlpManager + manager = YtDlpManager(str(data.url), uploader_backend) uploader_backend.key_prefix = f"{manager.id}@{data.quality}@" video_url = manager.download_video(data.quality) audio_url = manager.download_audio() return DownloadResponse(video=video_url, audio=audio_url) + @staticmethod + def download_v2(data: DownloadRequest): + filepath = None + try: + from app.utils.youtubeV2 import YtDlpManager + + manager = YtDlpManager(str(data.url)) + best_audio = manager.best_audio() + best_video = manager.best_video(data.quality) + filepath = manager.download(best_video, best_audio) + upload_backend = S3UploadBackend(f"{manager.id}@{data.quality}@") + video_url = upload_backend.upload(os.path.basename(filepath), filepath) + return DownloadResponseV2(video=video_url) + finally: + if filepath: + os.remove(filepath) + + @staticmethod + def resolutions(url: str): + from app.utils.youtubeV2 import YtDlpManager + manager = YtDlpManager(url) + return manager.resolutions + + @staticmethod + def filesize(data: DownloadRequest): + from app.utils.youtubeV2 import YtDlpManager + manager = YtDlpManager((data.url)) + video_size = manager.best_video(data.quality).filesize + audio_size = manager.best_audio().filesize + return {"filesize": video_size + audio_size} + class Files: @staticmethod diff --git a/app/tasks.py b/app/tasks.py index 3c4b9c5..26d05ab 100644 --- a/app/tasks.py +++ b/app/tasks.py @@ -11,8 +11,19 @@ from app.schemas import DownloadRequest retry_backoff=True, ) def download_youtube(self, url: str, quality: int) -> dict: - print("Get Task. Try to make them") request = DownloadRequest(url=url, quality=quality) response = YouTubeService.download(request) - print("Task make successfully. Return response") + return response.model_dump() + + +@celery_app.task( + bind=True, + name="download_youtube_v2", + autoretry_for=(Exception,), + retry_kwargs={"max_retries": 0}, + retry_backoff=True, +) +def download_youtube_v2(self, url: str, quality: int) -> dict: + request = DownloadRequest(url=url, quality=quality) + response = YouTubeService.download_v2(request) return response.model_dump() diff --git a/app/utils/uploader.py b/app/utils/uploader.py index 7f038d5..2523e3a 100644 --- a/app/utils/uploader.py +++ b/app/utils/uploader.py @@ -1,5 +1,6 @@ import os from abc import ABC, abstractmethod +from urllib.parse import quote import boto3 from botocore.client import Config @@ -180,9 +181,10 @@ class S3UploadBackend(UploadBackend): self.key_prefix = key_prefix def upload(self, name: str, file: bytes | str): - response = self.s3.upload_file( - Filename=file, - Bucket=self.bucket, - Key=f"{self.key_prefix}{name}", - ) - return response["Location"] + key = f"{self.key_prefix}{name}" + if isinstance(file, str): + self.s3.upload_file(Filename=file, Bucket=self.bucket, Key=key) + else: + self.s3.put_object(Bucket=self.bucket, Key=key, Body=file) + encoded_key = quote(key, "") + return f"{settings.S3_ENDPOINT_URL}/{self.bucket}/{encoded_key}" diff --git a/app/utils/youtubeV2.py b/app/utils/youtubeV2.py index 320db4c..5d24806 100644 --- a/app/utils/youtubeV2.py +++ b/app/utils/youtubeV2.py @@ -66,20 +66,20 @@ class YtDlpManager: return MediaInfo(f, f.get("format_id")) return None - def download(self, video_id: str | None = None, audio_id: str | None = None): - if video_id is None and audio_id is None: + def download(self, video: MediaInfo | None = None, audio: MediaInfo | None = None): + if video is None and audio is None: format_id = self.info.get( "format_id", ) else: - format_id = "" + str(video_id) if video_id is not None else "" - if audio_id is not None: + format_id = "" + str(video.id) if video is not None else "" + if audio is not None: if len(format_id) > 0: format_id += "+" - format_id += str(audio_id) + format_id += str(audio.id) ydl_opts = { - "format": f"{video_id}+{audio_id}", + "format": f"{format_id}", "merge_output_format": "mp4", "outtmpl": f"{settings.MEDIA_DIR}/%(title)s.%(ext)s", }