Добавить основную функциональность загрузки YouTube Shorts с использованием файла сессии и обработкой ошибок
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user