Добавил новый загрузчик c YouTube
This commit is contained in:
@@ -25,6 +25,12 @@ class ChunkUploadBackend(ABC):
|
|||||||
"""Прерывания загрузки"""
|
"""Прерывания загрузки"""
|
||||||
|
|
||||||
|
|
||||||
|
class UploadBackend(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def upload(self, name: str, file: bytes | str) -> str:
|
||||||
|
"""Загрузка файла"""
|
||||||
|
|
||||||
|
|
||||||
class DiskChunkUploadBackend(ChunkUploadBackend):
|
class DiskChunkUploadBackend(ChunkUploadBackend):
|
||||||
def __init__(self, key_prefix: str = ""):
|
def __init__(self, key_prefix: str = ""):
|
||||||
self.base_path = str(settings.MEDIA_DIR)
|
self.base_path = str(settings.MEDIA_DIR)
|
||||||
@@ -157,3 +163,26 @@ class HybridDiskS3UploadBackend(ChunkUploadBackend):
|
|||||||
)
|
)
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
return response["Location"]
|
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"]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
from typing import Literal
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from app.utils.uploader import ChunkUploadBackend
|
from app.utils.uploader import ChunkUploadBackend
|
||||||
|
|||||||
129
app/utils/youtubeV2.py
Normal file
129
app/utils/youtubeV2.py
Normal file
@@ -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
|
||||||
6
plans.md
Normal file
6
plans.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
- Реализовать ЭП, который возвращает список разрешений и размер файла
|
||||||
|
- Переделать загрузку (видео и аудио склеиваются) сперва скачать на диск, затем готовый файл отправить в S3 в случае если будет ошибка, поставить таймаут на 15 минут
|
||||||
|
- Реализовать статус доступности S3
|
||||||
|
- Перед загрузкой проверить нет ли случайно данное видео в S3 если есть дать на неё ссылку
|
||||||
Reference in New Issue
Block a user