223 lines
7.0 KiB
Python
223 lines
7.0 KiB
Python
"""
|
|
functionality:
|
|
- base class to make all calls to yt-dlp
|
|
- handle yt-dlp errors
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from http import cookiejar
|
|
from io import StringIO
|
|
|
|
import yt_dlp
|
|
from appsettings.src.config import AppConfig
|
|
from common.src.ta_redis import RedisArchivist
|
|
from django.conf import settings
|
|
|
|
|
|
class YtWrap:
|
|
"""wrap calls to yt"""
|
|
|
|
OBS_BASE = {
|
|
"default_search": "ytsearch",
|
|
"quiet": True,
|
|
"socket_timeout": 10,
|
|
"extractor_retries": 3,
|
|
"retries": 10,
|
|
}
|
|
|
|
def __init__(self, obs_request, config=False):
|
|
self.obs_request = obs_request
|
|
self.config = config
|
|
self.build_obs()
|
|
|
|
def build_obs(self):
|
|
"""build yt-dlp obs"""
|
|
self.obs = self.OBS_BASE.copy()
|
|
self.obs.update(self.obs_request)
|
|
if self.config:
|
|
self._add_cookie()
|
|
self._add_potoken()
|
|
|
|
if getattr(settings, "DEBUG", False):
|
|
del self.obs["quiet"]
|
|
print(self.obs)
|
|
|
|
def _add_cookie(self):
|
|
"""add cookie if enabled"""
|
|
if self.config["downloads"]["cookie_import"]:
|
|
cookie_io = CookieHandler(self.config).get()
|
|
self.obs["cookiefile"] = cookie_io
|
|
|
|
def _add_potoken(self):
|
|
"""add potoken if enabled"""
|
|
if self.config["downloads"].get("potoken"):
|
|
potoken = POTokenHandler(self.config).get()
|
|
self.obs.update(
|
|
{
|
|
"extractor_args": {
|
|
"youtube": {
|
|
"po_token": [potoken],
|
|
"player-client": ["mweb", "default"],
|
|
},
|
|
}
|
|
}
|
|
)
|
|
|
|
def download(self, url):
|
|
"""make download request"""
|
|
self.obs.update({"check_formats": "selected"})
|
|
with yt_dlp.YoutubeDL(self.obs) as ydl:
|
|
try:
|
|
ydl.download([url])
|
|
except yt_dlp.utils.DownloadError as err:
|
|
print(f"{url}: failed to download with message {err}")
|
|
if "Temporary failure in name resolution" in str(err):
|
|
raise ConnectionError("lost the internet, abort!") from err
|
|
|
|
return False, str(err)
|
|
|
|
self._validate_cookie()
|
|
|
|
return True, True
|
|
|
|
def extract(self, url) -> tuple[dict | None, str | None]:
|
|
"""
|
|
make extract request
|
|
returns response, error
|
|
"""
|
|
with yt_dlp.YoutubeDL(self.obs) as ydl:
|
|
try:
|
|
response = ydl.extract_info(url)
|
|
except cookiejar.LoadError as err:
|
|
print(f"cookie file is invalid: {err}")
|
|
return None, str(err)
|
|
except yt_dlp.utils.ExtractorError as err:
|
|
print(f"{url}: failed to extract: {err}, continue...")
|
|
return None, str(err)
|
|
except yt_dlp.utils.DownloadError as err:
|
|
if "This channel does not have a" in str(err):
|
|
return None, None
|
|
|
|
print(f"{url}: failed to get info from youtube: {err}")
|
|
if "Temporary failure in name resolution" in str(err):
|
|
raise ConnectionError("lost the internet, abort!") from err
|
|
|
|
return None, str(err)
|
|
|
|
self._validate_cookie()
|
|
|
|
return response, None
|
|
|
|
def _validate_cookie(self):
|
|
"""check cookie and write it back for next use"""
|
|
if not self.obs.get("cookiefile"):
|
|
return
|
|
|
|
new_cookie = self.obs["cookiefile"].read()
|
|
old_cookie = RedisArchivist().get_message_str("cookie")
|
|
if new_cookie and old_cookie != new_cookie:
|
|
print("refreshed stored cookie")
|
|
RedisArchivist().set_message("cookie", new_cookie, save=True)
|
|
|
|
|
|
class CookieHandler:
|
|
"""handle youtube cookie for yt-dlp"""
|
|
|
|
def __init__(self, config):
|
|
self.cookie_io = False
|
|
self.config = config
|
|
|
|
def get(self):
|
|
"""get cookie io stream"""
|
|
cookie = RedisArchivist().get_message_str("cookie")
|
|
self.cookie_io = StringIO(cookie)
|
|
return self.cookie_io
|
|
|
|
def set_cookie(self, cookie):
|
|
"""set cookie str and activate in config"""
|
|
cookie_clean = cookie.strip("\x00")
|
|
RedisArchivist().set_message("cookie", cookie_clean, save=True)
|
|
AppConfig().update_config({"downloads": {"cookie_import": True}})
|
|
self.config["downloads"]["cookie_import"] = True
|
|
print("[cookie]: activated and stored in Redis")
|
|
|
|
@staticmethod
|
|
def revoke():
|
|
"""revoke cookie"""
|
|
RedisArchivist().del_message("cookie")
|
|
RedisArchivist().del_message("cookie:valid")
|
|
AppConfig().update_config({"downloads": {"cookie_import": False}})
|
|
print("[cookie]: revoked")
|
|
|
|
def validate(self) -> bool:
|
|
"""validate cookie using the liked videos playlist"""
|
|
validation = RedisArchivist().get_message_dict("cookie:valid")
|
|
if validation:
|
|
print("[cookie]: used cached cookie validation")
|
|
return True
|
|
|
|
print("[cookie] validating cookie")
|
|
obs_request = {
|
|
"skip_download": True,
|
|
"extract_flat": True,
|
|
}
|
|
validator = YtWrap(obs_request, self.config)
|
|
response, error = validator.extract("LL")
|
|
self.store_validation(bool(response))
|
|
|
|
# update in redis to avoid expiring
|
|
modified = validator.obs["cookiefile"].getvalue().strip("\x00")
|
|
if modified:
|
|
cookie_clean = modified.strip("\x00")
|
|
RedisArchivist().set_message("cookie", cookie_clean)
|
|
|
|
if not response:
|
|
mess_dict = {
|
|
"status": "message:download",
|
|
"level": "error",
|
|
"title": "Cookie validation failed, exiting...",
|
|
"message": error,
|
|
}
|
|
RedisArchivist().set_message(
|
|
"message:download", mess_dict, expire=4
|
|
)
|
|
print("[cookie]: validation failed, exiting...")
|
|
|
|
print(f"[cookie]: validation success: {bool(response)}")
|
|
return bool(response)
|
|
|
|
@staticmethod
|
|
def store_validation(response):
|
|
"""remember last validation"""
|
|
now = datetime.now()
|
|
message = {
|
|
"status": response,
|
|
"validated": int(now.timestamp()),
|
|
"validated_str": now.strftime("%Y-%m-%d %H:%M"),
|
|
}
|
|
RedisArchivist().set_message("cookie:valid", message, expire=3600)
|
|
|
|
|
|
class POTokenHandler:
|
|
"""handle po token"""
|
|
|
|
REDIS_KEY = "potoken"
|
|
|
|
def __init__(self, config):
|
|
self.config = config
|
|
|
|
def get(self) -> str | None:
|
|
"""get PO token"""
|
|
potoken = RedisArchivist().get_message_str(self.REDIS_KEY)
|
|
return potoken
|
|
|
|
def set_token(self, new_token: str) -> None:
|
|
"""set new PO token"""
|
|
RedisArchivist().set_message(self.REDIS_KEY, new_token)
|
|
AppConfig().update_config({"downloads": {"potoken": True}})
|
|
|
|
def revoke_token(self) -> None:
|
|
"""revoke token"""
|
|
RedisArchivist().del_message(self.REDIS_KEY)
|
|
AppConfig().update_config({"downloads": {"potoken": False}})
|