Files
youtube-dl/my_youtube_dl.py

302 lines
10 KiB
Python
Raw Normal View History

from __future__ import unicode_literals
import os
import traceback
import tempfile
from glob import glob
from datetime import datetime
2021-05-02 00:36:04 +09:00
from threading import Thread
from enum import Enum
2026-01-06 19:25:41 +09:00
from typing import Optional, Dict, List, Any, Tuple, Union
2026-01-06 19:25:41 +09:00
import shutil as celery_shutil
from .setup import P
2022-07-10 23:22:28 +09:00
logger = P.logger
ModelSetting = P.ModelSetting
2026-01-06 19:25:41 +09:00
# 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):
2020-07-23 23:00:02 +09:00
READY = 0
START = 1
DOWNLOADING = 2
ERROR = 3
FINISHED = 4
STOP = 5
COMPLETED = 6
2026-01-06 19:25:41 +09:00
def __str__(self) -> str:
str_list: List[str] = ["준비", "분석중", "다운로드중", "실패", "변환중", "중지", "완료"]
2020-07-23 23:00:02 +09:00
return str_list[self.value]
2022-07-10 23:22:28 +09:00
class MyYoutubeDL:
2026-01-06 19:25:41 +09:00
DEFAULT_FILENAME: str = "%(title)s-%(id)s.%(ext)s"
2020-08-08 15:33:14 +09:00
2026-01-06 19:25:41 +09:00
_index: int = 0
2020-07-23 23:00:02 +09:00
2022-04-30 18:57:23 +09:00
def __init__(
self,
2026-01-06 19:25:41 +09:00
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,
2022-04-30 18:57:23 +09:00
):
2026-01-06 19:25:41 +09:00
# yt-dlp/youtube-dl의 utils 모듈에서 DateRange 임포트
2022-04-30 18:57:23 +09:00
DateRange = __import__(
f"{youtube_dl_package}.utils", fromlist=["DateRange"]
).DateRange
2020-11-08 22:58:35 +09:00
2020-07-23 23:00:02 +09:00
if save_path is None:
save_path = temp_path
if opts is None:
opts = {}
2026-01-06 19:25:41 +09:00
self.plugin: str = plugin
self.type: str = type_name
self.url: str = url
self.filename: str = filename
# 임시 폴더 생성
2020-07-23 23:00:02 +09:00
if not os.path.isdir(temp_path):
os.makedirs(temp_path)
2026-01-06 19:25:41 +09:00
self.temp_path: str = tempfile.mkdtemp(prefix="youtube-dl_", dir=temp_path)
# 저장 폴더 생성
2020-07-23 23:00:02 +09:00
if not os.path.isdir(save_path):
os.makedirs(save_path)
2026-01-06 19:25:41 +09:00
self.save_path: str = save_path
self.opts: Dict[str, Any] = opts
2020-11-08 22:58:35 +09:00
if dateafter or datebefore:
2022-04-30 18:57:23 +09:00
self.opts["daterange"] = DateRange(start=dateafter, end=datebefore)
2026-01-06 19:25:41 +09:00
self.index: int = MyYoutubeDL._index
2021-07-15 23:13:22 +09:00
MyYoutubeDL._index += 1
2026-01-06 19:25:41 +09:00
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,
2020-07-23 23:00:02 +09:00
}
2026-01-06 19:25:41 +09:00
# 진행률 정보
self.progress_hooks: Dict[str, Optional[Union[int, float, str]]] = {
"downloaded_bytes": None,
"total_bytes": None,
"eta": None,
"speed": None,
2020-07-23 23:00:02 +09:00
}
2026-01-06 19:25:41 +09:00
def start(self) -> bool:
"""다운로드 스레드 시작"""
2020-07-23 23:00:02 +09:00
if self.status != Status.READY:
return False
2021-07-15 23:13:22 +09:00
self._thread = Thread(target=self.run)
self._thread.start()
2020-07-23 23:00:02 +09:00
return True
2026-01-06 19:25:41 +09:00
def run(self) -> None:
"""다운로드 실행 본체"""
2022-04-30 18:57:23 +09:00
youtube_dl = __import__(youtube_dl_package)
2020-11-08 22:58:35 +09:00
2020-07-23 23:00:02 +09:00
try:
self.start_time = datetime.now()
self.status = Status.START
2026-01-06 19:25:41 +09:00
# 정보 추출
info_dict = MyYoutubeDL.get_info_dict(
self.url,
self.opts.get("proxy"),
self.opts.get("cookiefile"),
self.opts.get("http_headers"),
2026-01-06 19:25:41 +09:00
self.opts.get("cookiesfrombrowser"),
2022-04-30 18:57:23 +09:00
)
2026-01-06 19:25:41 +09:00
if info_dict is None:
2020-07-23 23:00:02 +09:00
self.status = Status.ERROR
return
2026-01-06 19:25:41 +09:00
self.info_dict["extractor"] = info_dict.get("extractor", "unknown")
self.info_dict["title"] = info_dict.get("title", info_dict.get("id", "unknown"))
2022-04-30 18:57:23 +09:00
self.info_dict["uploader"] = info_dict.get("uploader", "")
self.info_dict["uploader_url"] = info_dict.get("uploader_url", "")
2026-01-06 19:25:41 +09:00
ydl_opts: Dict[str, Any] = {
2022-04-30 18:57:23 +09:00
"logger": MyLogger(),
"progress_hooks": [self.my_hook],
"outtmpl": os.path.join(self.temp_path, self.filename),
"ignoreerrors": True,
"cachedir": False,
2026-01-06 19:25:41 +09:00
"nocheckcertificate": True,
2020-07-23 23:00:02 +09:00
}
2026-01-06 19:25:41 +09:00
# yt-dlp 전용 성능 향상 옵션
if youtube_dl_package == "yt_dlp":
ydl_opts.update({
"concurrent_fragment_downloads": 5,
"retries": 10,
})
2020-07-23 23:00:02 +09:00
ydl_opts.update(self.opts)
2026-01-06 19:25:41 +09:00
2020-07-23 23:00:02 +09:00
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
2026-01-06 19:25:41 +09:00
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):
# 임시 폴더의 파일을 실제 저장 경로로 이동
2022-04-30 18:57:23 +09:00
for i in glob(self.temp_path + "/**/*", recursive=True):
2026-01-06 19:25:41 +09:00
path: str = i.replace(self.temp_path, self.save_path, 1)
2020-07-23 23:00:02 +09:00
if os.path.isdir(i):
if not os.path.isdir(path):
os.mkdir(path)
continue
celery_shutil.move(i, path)
2020-07-23 23:00:02 +09:00
self.status = Status.COMPLETED
2022-05-05 21:34:12 +09:00
except Exception as error:
2020-07-23 23:00:02 +09:00
self.status = Status.ERROR
2026-01-06 19:25:41 +09:00
logger.error(f"실행 중 예외 발생: {error}")
2020-07-23 23:00:02 +09:00
logger.error(traceback.format_exc())
finally:
2026-01-06 19:25:41 +09:00
if os.path.exists(self.temp_path):
celery_shutil.rmtree(self.temp_path)
2020-07-23 23:00:02 +09:00
if self.status != Status.STOP:
self.end_time = datetime.now()
2026-01-06 19:25:41 +09:00
def stop(self) -> bool:
"""다운로드 중지"""
2020-07-23 23:00:02 +09:00
if self.status in (Status.ERROR, Status.STOP, Status.COMPLETED):
return False
self.status = Status.STOP
self.end_time = datetime.now()
return True
@staticmethod
2026-01-06 19:25:41 +09:00
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__(
2022-04-30 18:57:23 +09:00
f"{youtube_dl_package}.version", fromlist=["__version__"]
).__version__
2020-11-08 22:58:35 +09:00
return __version__
2020-07-23 23:00:02 +09:00
@staticmethod
2026-01-06 19:25:41 +09:00
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]]:
"""비디오 메타데이터 정보 추출"""
2022-04-30 18:57:23 +09:00
youtube_dl = __import__(youtube_dl_package)
2020-07-23 23:00:02 +09:00
try:
2026-01-06 19:25:41 +09:00
ydl_opts: Dict[str, Any] = {
"extract_flat": "in_playlist",
"logger": MyLogger(),
"nocheckcertificate": True,
}
2020-07-23 23:00:02 +09:00
if proxy:
2022-04-30 18:57:23 +09:00
ydl_opts["proxy"] = proxy
if cookiefile:
2022-04-30 18:57:23 +09:00
ydl_opts["cookiefile"] = cookiefile
if http_headers:
ydl_opts["http_headers"] = http_headers
2026-01-06 19:25:41 +09:00
if cookiesfrombrowser:
ydl_opts["cookiesfrombrowser"] = (cookiesfrombrowser, None, None, None)
2020-07-23 23:00:02 +09:00
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
2026-01-06 19:25:41 +09:00
info: Dict[str, Any] = ydl.extract_info(url, download=False)
2022-05-05 21:34:12 +09:00
except Exception as error:
2026-01-06 19:25:41 +09:00
logger.error(f"정보 추출 중 예외 발생: {error}")
2020-07-23 23:00:02 +09:00
logger.error(traceback.format_exc())
return None
return ydl.sanitize_info(info)
2020-07-23 23:00:02 +09:00
2026-01-06 19:25:41 +09:00
def my_hook(self, data: Dict[str, Any]) -> None:
"""진행률 업데이트 훅"""
2020-07-23 23:00:02 +09:00
if self.status != Status.STOP:
2026-01-06 19:25:41 +09:00
if data["status"] == "downloading":
self.status = Status.DOWNLOADING
elif data["status"] == "error":
self.status = Status.ERROR
elif data["status"] == "finished":
self.status = Status.FINISHED
2022-05-05 21:34:12 +09:00
if data["status"] != "error":
2026-01-06 19:25:41 +09:00
self.filename = os.path.basename(data.get("filename", self.filename))
2022-05-05 21:34:12 +09:00
self.progress_hooks["downloaded_bytes"] = data.get("downloaded_bytes")
2026-01-06 19:25:41 +09:00
self.progress_hooks["total_bytes"] = data.get("total_bytes") or data.get("total_bytes_estimate")
2022-05-05 21:34:12 +09:00
self.progress_hooks["eta"] = data.get("eta")
self.progress_hooks["speed"] = data.get("speed")
2020-07-23 23:00:02 +09:00
@property
2026-01-06 19:25:41 +09:00
def status(self) -> Status:
2021-07-15 23:13:22 +09:00
return self._status
2020-07-23 23:00:02 +09:00
@status.setter
2026-01-06 19:25:41 +09:00
def status(self, value: Status) -> None:
2021-07-15 23:13:22 +09:00
self._status = value
2026-01-06 19:25:41 +09:00
# 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}")
2020-03-19 00:32:24 +09:00
2022-07-10 23:22:28 +09:00
class MyLogger:
2026-01-06 19:25:41 +09:00
"""yt-dlp의 로그를 가로채서 처리하는 클래성"""
def debug(self, msg: str) -> None:
# 진행 상황 관련 로그는 걸러냄
if " ETA " in msg or "at" in msg and "B/s" in msg:
return
2020-07-23 23:00:02 +09:00
logger.debug(msg)
2026-01-06 19:25:41 +09:00
def warning(self, msg: str) -> None:
2020-07-23 23:00:02 +09:00
logger.warning(msg)
2026-01-06 19:25:41 +09:00
def error(self, msg: str) -> None:
2020-07-23 23:00:02 +09:00
logger.error(msg)