Files
youtube-dl/my_youtube_dl.py

302 lines
10 KiB
Python

from __future__ import unicode_literals
import os
import traceback
import tempfile
from glob import glob
from datetime import datetime
from threading import Thread
from enum import Enum
from typing import Optional, Dict, List, Any, Tuple, Union
import shutil as celery_shutil
from .setup import P
logger = P.logger
ModelSetting = P.ModelSetting
# yt-dlp 패키지 설정 (기본값 index 1)
package_idx: str = ModelSetting.get("youtube_dl_package")
if not package_idx:
package_idx = "1"
youtube_dl_package: str = P.youtube_dl_packages[int(package_idx)].replace("-", "_")
class Status(Enum):
READY = 0
START = 1
DOWNLOADING = 2
ERROR = 3
FINISHED = 4
STOP = 5
COMPLETED = 6
def __str__(self) -> str:
str_list: List[str] = ["준비", "분석중", "다운로드중", "실패", "변환중", "중지", "완료"]
return str_list[self.value]
class MyYoutubeDL:
DEFAULT_FILENAME: str = "%(title)s-%(id)s.%(ext)s"
_index: int = 0
def __init__(
self,
plugin: str,
type_name: str,
url: str,
filename: str,
temp_path: str,
save_path: Optional[str] = None,
opts: Optional[Dict[str, Any]] = None,
dateafter: Optional[str] = None,
datebefore: Optional[str] = None,
):
# yt-dlp/youtube-dl의 utils 모듈에서 DateRange 임포트
DateRange = __import__(
f"{youtube_dl_package}.utils", fromlist=["DateRange"]
).DateRange
if save_path is None:
save_path = temp_path
if opts is None:
opts = {}
self.plugin: str = plugin
self.type: str = type_name
self.url: str = url
self.filename: str = filename
# 임시 폴더 생성
if not os.path.isdir(temp_path):
os.makedirs(temp_path)
self.temp_path: str = tempfile.mkdtemp(prefix="youtube-dl_", dir=temp_path)
# 저장 폴더 생성
if not os.path.isdir(save_path):
os.makedirs(save_path)
self.save_path: str = save_path
self.opts: Dict[str, Any] = opts
if dateafter or datebefore:
self.opts["daterange"] = DateRange(start=dateafter, end=datebefore)
self.index: int = MyYoutubeDL._index
MyYoutubeDL._index += 1
self._status: Status = Status.READY
self._thread: Optional[Thread] = None
self.key: Optional[str] = None
self.start_time: Optional[datetime] = None
self.end_time: Optional[datetime] = None
# 비디오 정보
self.info_dict: Dict[str, Optional[str]] = {
"extractor": None,
"title": None,
"uploader": None,
"uploader_url": None,
}
# 진행률 정보
self.progress_hooks: Dict[str, Optional[Union[int, float, str]]] = {
"downloaded_bytes": None,
"total_bytes": None,
"eta": None,
"speed": None,
}
def start(self) -> bool:
"""다운로드 스레드 시작"""
if self.status != Status.READY:
return False
self._thread = Thread(target=self.run)
self._thread.start()
return True
def run(self) -> None:
"""다운로드 실행 본체"""
youtube_dl = __import__(youtube_dl_package)
try:
self.start_time = datetime.now()
self.status = Status.START
# 정보 추출
info_dict = MyYoutubeDL.get_info_dict(
self.url,
self.opts.get("proxy"),
self.opts.get("cookiefile"),
self.opts.get("http_headers"),
self.opts.get("cookiesfrombrowser"),
)
if info_dict is None:
self.status = Status.ERROR
return
self.info_dict["extractor"] = info_dict.get("extractor", "unknown")
self.info_dict["title"] = info_dict.get("title", info_dict.get("id", "unknown"))
self.info_dict["uploader"] = info_dict.get("uploader", "")
self.info_dict["uploader_url"] = info_dict.get("uploader_url", "")
ydl_opts: Dict[str, Any] = {
"logger": MyLogger(),
"progress_hooks": [self.my_hook],
"outtmpl": os.path.join(self.temp_path, self.filename),
"ignoreerrors": True,
"cachedir": False,
"nocheckcertificate": True,
}
# yt-dlp 전용 성능 향상 옵션
if youtube_dl_package == "yt_dlp":
ydl_opts.update({
"concurrent_fragment_downloads": 5,
"retries": 10,
})
ydl_opts.update(self.opts)
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
logger.debug(f"다운로드 시작: {self.url}")
error_code: int = ydl.download([self.url])
logger.debug(f"다운로드 종료 (코드: {error_code})")
if self.status in (Status.START, Status.FINISHED, Status.DOWNLOADING):
# 임시 폴더의 파일을 실제 저장 경로로 이동
for i in glob(self.temp_path + "/**/*", recursive=True):
path: str = i.replace(self.temp_path, self.save_path, 1)
if os.path.isdir(i):
if not os.path.isdir(path):
os.mkdir(path)
continue
celery_shutil.move(i, path)
self.status = Status.COMPLETED
except Exception as error:
self.status = Status.ERROR
logger.error(f"실행 중 예외 발생: {error}")
logger.error(traceback.format_exc())
finally:
if os.path.exists(self.temp_path):
celery_shutil.rmtree(self.temp_path)
if self.status != Status.STOP:
self.end_time = datetime.now()
def stop(self) -> bool:
"""다운로드 중지"""
if self.status in (Status.ERROR, Status.STOP, Status.COMPLETED):
return False
self.status = Status.STOP
self.end_time = datetime.now()
return True
@staticmethod
def get_preview_url(url: str) -> Optional[str]:
"""미리보기용 직접 재생 가능한 URL 추출"""
youtube_dl = __import__(youtube_dl_package)
try:
# 미리보기를 위해 포맷 필터링 (mp4, 비디오+오디오 권장)
ydl_opts: Dict[str, Any] = {
"format": "best[ext=mp4]/best",
"logger": MyLogger(),
"nocheckcertificate": True,
"quiet": True,
}
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
info: Dict[str, Any] = ydl.extract_info(url, download=False)
return info.get("url")
except Exception as error:
logger.error(f"미리보기 URL 추출 중 예외 발생: {error}")
return None
@staticmethod
def get_version() -> str:
"""라이브러리 버전 확인"""
__version__: str = __import__(
f"{youtube_dl_package}.version", fromlist=["__version__"]
).__version__
return __version__
@staticmethod
def get_info_dict(
url: str,
proxy: Optional[str] = None,
cookiefile: Optional[str] = None,
http_headers: Optional[Dict[str, str]] = None,
cookiesfrombrowser: Optional[str] = None
) -> Optional[Dict[str, Any]]:
"""비디오 메타데이터 정보 추출"""
youtube_dl = __import__(youtube_dl_package)
try:
ydl_opts: Dict[str, Any] = {
"extract_flat": "in_playlist",
"logger": MyLogger(),
"nocheckcertificate": True,
}
if proxy:
ydl_opts["proxy"] = proxy
if cookiefile:
ydl_opts["cookiefile"] = cookiefile
if http_headers:
ydl_opts["http_headers"] = http_headers
if cookiesfrombrowser:
ydl_opts["cookiesfrombrowser"] = (cookiesfrombrowser, None, None, None)
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
info: Dict[str, Any] = ydl.extract_info(url, download=False)
except Exception as error:
logger.error(f"정보 추출 중 예외 발생: {error}")
logger.error(traceback.format_exc())
return None
return ydl.sanitize_info(info)
def my_hook(self, data: Dict[str, Any]) -> None:
"""진행률 업데이트 훅"""
if self.status != Status.STOP:
if data["status"] == "downloading":
self.status = Status.DOWNLOADING
elif data["status"] == "error":
self.status = Status.ERROR
elif data["status"] == "finished":
self.status = Status.FINISHED
if data["status"] != "error":
self.filename = os.path.basename(data.get("filename", self.filename))
self.progress_hooks["downloaded_bytes"] = data.get("downloaded_bytes")
self.progress_hooks["total_bytes"] = data.get("total_bytes") or data.get("total_bytes_estimate")
self.progress_hooks["eta"] = data.get("eta")
self.progress_hooks["speed"] = data.get("speed")
@property
def status(self) -> Status:
return self._status
@status.setter
def status(self, value: Status) -> None:
self._status = value
# Socket.IO를 통한 상태 업데이트 전송
try:
basic_module = P.get_module('basic')
if basic_module:
basic_module.socketio_emit("status", self)
except Exception as e:
logger.error(f"SocketIO 전송 에러: {e}")
class MyLogger:
"""yt-dlp의 로그를 가로채서 처리하는 클래성"""
def debug(self, msg: str) -> None:
# 진행 상황 관련 로그는 걸러냄
if " ETA " in msg or "at" in msg and "B/s" in msg:
return
logger.debug(msg)
def warning(self, msg: str) -> None:
logger.warning(msg)
def error(self, msg: str) -> None:
logger.error(msg)