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()