From 8ffa8cdf71a05090af9520b9c62314fdfcd1d768 Mon Sep 17 00:00:00 2001 From: Viner Abubakirov Date: Wed, 25 Feb 2026 10:56:28 +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=B9=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D1=87=D0=B8=D0=BA=20c=20YouTube?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/utils/uploader.py | 29 +++++++++ app/utils/youtube.py | 1 - app/utils/youtubeV2.py | 129 +++++++++++++++++++++++++++++++++++++++++ plans.md | 6 ++ 4 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 app/utils/youtubeV2.py create mode 100644 plans.md diff --git a/app/utils/uploader.py b/app/utils/uploader.py index 7798020..7f038d5 100644 --- a/app/utils/uploader.py +++ b/app/utils/uploader.py @@ -25,6 +25,12 @@ class ChunkUploadBackend(ABC): """Прерывания загрузки""" +class UploadBackend(ABC): + @abstractmethod + def upload(self, name: str, file: bytes | str) -> str: + """Загрузка файла""" + + class DiskChunkUploadBackend(ChunkUploadBackend): def __init__(self, key_prefix: str = ""): self.base_path = str(settings.MEDIA_DIR) @@ -157,3 +163,26 @@ class HybridDiskS3UploadBackend(ChunkUploadBackend): ) os.remove(filepath) return response["Location"] + + +class S3UploadBackend(UploadBackend): + def __init__(self, key_prefix=""): + self.s3 = boto3.client( + service_name="s3", + aws_access_key_id=settings.S3_ACCESS_KEY, + aws_secret_access_key=settings.S3_SECRET_KEY, + endpoint_url=settings.S3_ENDPOINT_URL, + region_name=settings.S3_REGION_NAME, + use_ssl=True, + config=Config(signature_version=settings.S3_SIGNATURE_VERSION), + ) + self.bucket = settings.S3_BUCKET_NAME + 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"] diff --git a/app/utils/youtube.py b/app/utils/youtube.py index 00e913c..6bda70e 100644 --- a/app/utils/youtube.py +++ b/app/utils/youtube.py @@ -1,5 +1,4 @@ import subprocess -from typing import Literal from dataclasses import dataclass from app.utils.uploader import ChunkUploadBackend diff --git a/app/utils/youtubeV2.py b/app/utils/youtubeV2.py new file mode 100644 index 0000000..320db4c --- /dev/null +++ b/app/utils/youtubeV2.py @@ -0,0 +1,129 @@ +import yt_dlp +from app.core.config import settings + +class MediaInfo: + def __init__(self, format: dict, id: int): + self.__format = format + self.id = id + + @property + def filesize(self): + return self.__format.get("filesize") + + @property + def codec(self): + if vcodec := self.__format.get("vcodec"): + return vcodec + return self.__format.get("acodec") + + +class YtDlpManager: + def __init__(self, url: str): + self.url = url + self._extract_info() + self._set_video_codecs() + + @property + def resolutions(self): + return sorted(self._resolutions.keys()) + + def best_audio(self) -> MediaInfo | None: + """ + Получает аудио дорожку с наилучшим качеством звучания + """ + if self.info.get("acodec") in ("none", None): + return None + ids = str(self.info.get("format_id", "")).split("+") + if len(ids) == 2: + audio_id = ids[1] + else: + audio_id = ids[0] + + for f in self.info["formats"]: + if f.get("format_id") == audio_id: + return MediaInfo(f, audio_id) + return None + + def best_video(self, height: int | None = None, codec: str | None = None): + """ + Возвращает видео дорожку с наилучшим качеством с указанными параметрами + """ + if height is None: + if codec is None: + height = self.info.get("height") + else: + height = max(self._codecs.get(codec, [0])) + + if not self._video_exist(codec, height): + return None + + iterator = reversed(self.info["formats"]) + for f in iterator: + if f.get("height", 0) == height: + if codec is None: + return MediaInfo(f, f.get("format_id")) + if f.get("codec") == codec: + 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: + 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: + if len(format_id) > 0: + format_id += "+" + format_id += str(audio_id) + + ydl_opts = { + "format": f"{video_id}+{audio_id}", + "merge_output_format": "mp4", + "outtmpl": f"{settings.MEDIA_DIR}/%(title)s.%(ext)s", + } + + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(self.url, download=True) + return ydl.prepare_filename(info) + + def _extract_info(self): + ydl_opts = { + "quiet": True, + "no_warnings": True, + "extract_flat": False, + } + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + self.info = ydl.extract_info(self.url, download=False) + if self.info is None: + raise Exception("Не удалось получить информацию о видео") + + def _set_video_codecs(self): + self._codecs: dict[str, list[int]] = {} + self._resolutions: dict[int, list[str]] = {} + for f in self.info["formats"]: + codec = f.get("vcodec") + if codec in ("none", None): + continue + + height = f.get("height") + if height not in self._resolutions: + self._resolutions[height] = [] + if codec not in self._codecs: + self._codecs[codec] = [] + self._codecs[codec].append(height) + self._resolutions[height].append(codec) + + def _video_exist(self, codec: str = "", resolution: int = 0): + if codec: + if resolution: + if resolutions := self._codecs.get(codec, []): + return resolution in resolutions + return True + if resolution: + if codec: + if codecs := self._resolutions.get(resolution, []): + return codec in codecs + return True + return False diff --git a/plans.md b/plans.md new file mode 100644 index 0000000..22f00d3 --- /dev/null +++ b/plans.md @@ -0,0 +1,6 @@ + + +- Реализовать ЭП, который возвращает список разрешений и размер файла +- Переделать загрузку (видео и аудио склеиваются) сперва скачать на диск, затем готовый файл отправить в S3 в случае если будет ошибка, поставить таймаут на 15 минут +- Реализовать статус доступности S3 +- Перед загрузкой проверить нет ли случайно данное видео в S3 если есть дать на неё ссылку