Files
yt-shorts-downloader/src/yt_shorts_downloader/session.py
T

186 lines
5.5 KiB
Python

from __future__ import annotations
import time
from pathlib import Path
from .models import SessionCookie, SessionValidation
_SESSION_DOMAIN_SUFFIXES = ("youtube.com", "google.com")
_AUTH_COOKIE_NAMES = {
"SID",
"HSID",
"SSID",
"APISID",
"SAPISID",
"__Secure-1PSID",
"__Secure-3PSID",
"LOGIN_INFO",
}
_AUTH_COOKIE_ANCHORS = {
"SID",
"SAPISID",
"__Secure-1PSID",
"__Secure-3PSID",
"LOGIN_INFO",
}
def validate_session_file(path: Path) -> SessionValidation:
normalized_path = path.expanduser().resolve()
if not normalized_path.exists():
return SessionValidation(
exists=False,
structurally_valid=False,
fresh=False,
is_usable=False,
message=f"Файл сессии не найден: {normalized_path}",
)
try:
cookies = _parse_session_file(normalized_path)
except ValueError as exc:
return SessionValidation(
exists=True,
structurally_valid=False,
fresh=False,
is_usable=False,
message=f"Файл сессии не прошёл проверку формата: {exc}",
)
if not cookies:
return SessionValidation(
exists=True,
structurally_valid=False,
fresh=False,
is_usable=False,
message="Файл сессии пустой",
)
relevant_cookies = [
cookie
for cookie in cookies
if _matches_domain(cookie.domain, _SESSION_DOMAIN_SUFFIXES)
]
if not relevant_cookies:
return SessionValidation(
exists=True,
structurally_valid=False,
fresh=False,
is_usable=False,
message="В файле сессии нет записей для YouTube или Google",
)
now = int(time.time())
fresh_auth_names: set[str] = set()
soonest_expiry: int | None = None
for cookie in relevant_cookies:
if cookie.name not in _AUTH_COOKIE_NAMES:
continue
if cookie.expires != 0 and cookie.expires <= now:
continue
fresh_auth_names.add(cookie.name)
if cookie.expires > 0 and (
soonest_expiry is None or cookie.expires < soonest_expiry
):
soonest_expiry = cookie.expires
anchor_count = len(fresh_auth_names & _AUTH_COOKIE_ANCHORS)
fresh = len(fresh_auth_names) >= 3 and anchor_count >= 1
if not fresh:
return SessionValidation(
exists=True,
structurally_valid=True,
fresh=False,
is_usable=False,
message=(
"Файл сессии в корректном формате, но в нём нет достаточного набора "
"актуальных YouTube auth-cookie"
),
)
message = (
"Файл сессии валиден и выглядит актуальным для YouTube: "
f"найдено {len(fresh_auth_names)} свежих auth-cookie"
)
if soonest_expiry is not None:
hours_left = max((soonest_expiry - now) // 3600, 0)
message += f", ближайшее истечение примерно через {hours_left} ч"
return SessionValidation(
exists=True,
structurally_valid=True,
fresh=True,
is_usable=True,
message=message,
)
def _parse_session_file(path: Path) -> list[SessionCookie]:
cookies: list[SessionCookie] = []
with path.open("r", encoding="utf-8") as handle:
for line_number, raw_line in enumerate(handle, start=1):
stripped = raw_line.strip()
if not stripped:
continue
is_httponly_cookie = stripped.startswith("#HttpOnly_")
if stripped.startswith("#") and not is_httponly_cookie:
continue
if is_httponly_cookie:
stripped = stripped.removeprefix("#HttpOnly_")
parts = stripped.split("\t")
if len(parts) != 7:
raise ValueError(
"Строка "
f"{line_number}: ожидалось 7 колонок Netscape cookie file, "
f"получено {len(parts)}"
)
(
domain,
include_subdomains,
cookie_path,
secure,
expires,
name,
value,
) = parts
try:
expires_at = int(expires)
except ValueError as exc:
raise ValueError(
f"Строка {line_number}: expires должен быть UNIX timestamp"
) from exc
cookies.append(
SessionCookie(
domain=domain,
include_subdomains=include_subdomains.upper() == "TRUE",
path=cookie_path,
secure=secure.upper() == "TRUE",
expires=expires_at,
name=name,
value=value,
)
)
return cookies
def _matches_domain(domain: str, suffixes: tuple[str, ...]) -> bool:
normalized_domain = _normalize_domain(domain)
return any(
normalized_domain == suffix or normalized_domain.endswith(f".{suffix}")
for suffix in suffixes
)
def _normalize_domain(domain: str) -> str:
return domain.removeprefix("#HttpOnly_").lstrip(".").lower()