import json import re import urllib.parse import urllib.request VIDEO_ID_PATTERN = re.compile(r"^[\w-]{11}$") YOUTUBE_HOSTS = {"youtube.com", "www.youtube.com", "m.youtube.com", "music.youtube.com", "youtu.be"} PATH_PREFIXES = ("/shorts/", "/embed/", "/live/") PLAYER_RESPONSE_MARKER = "ytInitialPlayerResponse = " TOPIC_SUFFIX = " - Topic" REQUEST_HEADERS = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0", "Accept-Language": "en-US,en;q=0.9", "Cookie": "CONSENT=YES+1", } def video_id(text: str) -> str | None: text = text.strip() if VIDEO_ID_PATTERN.match(text): return text url = urllib.parse.urlparse(text if "://" in text else f"https://{text}") if url.hostname not in YOUTUBE_HOSTS: return None if url.hostname == "youtu.be": candidate = url.path.strip("/").split("/")[0] elif url.path == "/watch": candidate = urllib.parse.parse_qs(url.query).get("v", [""])[0] else: prefix = next((prefix for prefix in PATH_PREFIXES if url.path.startswith(prefix)), None) candidate = url.path[len(prefix):].split("/")[0] if prefix else "" return candidate if VIDEO_ID_PATTERN.match(candidate) else None def watch_url(identifier: str) -> str: return f"https://music.youtube.com/watch?v={identifier}" def cover_url(identifier: str) -> str: return f"https://i.ytimg.com/vi/{identifier}/hqdefault.jpg" def oembed(identifier: str) -> dict: query = urllib.parse.urlencode({"url": f"https://www.youtube.com/watch?v={identifier}", "format": "json"}) try: with urllib.request.urlopen(f"https://www.youtube.com/oembed?{query}", timeout=10) as response: return json.load(response) except (OSError, ValueError): return {} def player_response(identifier: str) -> dict: request = urllib.request.Request(f"https://www.youtube.com/watch?v={identifier}", headers=REQUEST_HEADERS) try: with urllib.request.urlopen(request, timeout=15) as response: page = response.read().decode("utf-8", errors="replace") except OSError: return {} start = page.find(PLAYER_RESPONSE_MARKER) if start == -1: return {} try: data, _ = json.JSONDecoder().raw_decode(page, start + len(PLAYER_RESPONSE_MARKER)) except ValueError: return {} return data if isinstance(data, dict) else {} def clean_artist(name: str | None) -> str | None: if name and name.endswith(TOPIC_SUFFIX): return name[: -len(TOPIC_SUFFIX)] return name def as_number(value) -> int | None: try: return int(value) except (TypeError, ValueError): return None def video_details(identifier: str) -> dict: response = player_response(identifier) details = response.get("videoDetails") or {} microformat = (response.get("microformat") or {}).get("playerMicroformatRenderer") or {} fallback = oembed(identifier) if not details.get("title") else {} return { "id": identifier, "title": details.get("title") or fallback.get("title"), "artist": clean_artist(details.get("author") or fallback.get("author_name")), "durationSeconds": as_number(details.get("lengthSeconds")), "views": as_number(details.get("viewCount")), "published": (microformat.get("publishDate") or "")[:10] or None, "description": details.get("shortDescription") or None, "cover": cover_url(identifier), "url": watch_url(identifier), } def format_duration(seconds: int | None) -> str: if seconds is None: return "-" hours, remainder = divmod(seconds, 3600) minutes, seconds = divmod(remainder, 60) return f"{hours}:{minutes:02}:{seconds:02}" if hours else f"{minutes}:{seconds:02}"