iptv/M3U8/scrapers/utils/config.py

269 lines
8.2 KiB
Python
Raw Normal View History

2025-09-03 15:00:17 -04:00
import asyncio
2025-09-04 19:53:27 -04:00
import json
2025-09-03 15:00:17 -04:00
import logging
2025-09-04 19:53:27 -04:00
import re
2025-09-08 12:02:36 -04:00
from collections.abc import Callable
2025-09-04 09:59:19 -04:00
from datetime import datetime
2025-09-03 15:00:17 -04:00
from pathlib import Path
from typing import Any
import httpx
import pytz
2025-09-11 14:56:17 -04:00
from playwright.async_api import Browser, BrowserContext, Playwright, Request
2025-09-03 15:00:17 -04:00
TZ = pytz.timezone("America/New_York")
2025-09-04 09:59:19 -04:00
now = datetime.now(TZ)
2025-09-11 14:55:53 -04:00
UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0"
)
2025-09-03 15:00:17 -04:00
LOGOS = {
2025-09-16 15:55:08 -04:00
"Bundesliga": "https://1000logos.net/wp-content/uploads/2020/09/Bundesliga-Logo-500x313.png",
"La Liga": "https://1000logos.net/wp-content/uploads/2019/01/Spanish-La-Liga-Logo-500x281.png",
"Ligue 1": "https://1000logos.net/wp-content/uploads/2019/01/Ligue-1-Logo-500x281.png",
"MLB": "https://1000logos.net/wp-content/uploads/2017/04/MLB-Logo-500x281.png",
"MLS": "https://1000logos.net/wp-content/uploads/2017/10/MLS-logo-500x393.png",
"NBA": "https://1000logos.net/wp-content/uploads/2025/08/Jerry-West-the-NBA-Logo-500x281.png",
"NCAA": "https://1000logos.net/wp-content/uploads/2021/12/NCAA-Logo-500x281.png",
"NFL": "https://1000logos.net/wp-content/uploads/2017/05/NFL-logo-500x338.png",
"NHL": "https://1000logos.net/wp-content/uploads/2017/05/NHL-Logo-500x333.png",
"Premier League": "https://1000logos.net/wp-content/uploads/2017/05/Premier-League-logo-500x210.png",
2025-09-16 20:40:48 -04:00
"Primera A": "https://b.fssta.com/uploads/application/soccer/competition-logos/ColombianPrimeraA.png",
2025-09-16 15:55:08 -04:00
"Primeira Liga": "https://1000logos.net/wp-content/uploads/2022/01/Portuguese-Primeira-Liga-logo-500x281.png",
"Serie A": " https://1000logos.net/wp-content/uploads/2019/01/Italian-Serie-A-Logo-500x281.png",
"UEFA Champions League": "https://1000logos.net/wp-content/uploads/2022/01/UEFA-Champions-League-logo-500x281.png",
"WNBA": "https://1000logos.net/wp-content/uploads/2018/09/logo-wnba-500x287.png",
2025-09-16 20:40:48 -04:00
"Wrestling": "https://1000logos.net/wp-content/uploads/2017/05/WWE-logo-500x464.png",
2025-09-16 15:55:08 -04:00
"default": "https://i.gyazo.com/978f2eb4a199ca5b56b447aded0cb9e3.png",
2025-09-03 15:00:17 -04:00
}
2025-09-13 21:04:01 -04:00
alias_map = {
2025-09-16 20:40:48 -04:00
"MLS": ["Major League Soccer"],
2025-09-16 15:55:08 -04:00
"NCAA": ["CBB", "CFB", "NCAAB", "NCAAF"],
2025-09-13 21:04:01 -04:00
"Premier League": ["EPL"],
2025-09-16 15:55:08 -04:00
"UEFA Champions League": ["Champions League", "UCL"],
2025-09-16 20:40:48 -04:00
"WNBA": ["NBA W"],
"Wrestling": ["WWE"],
2025-09-13 21:04:01 -04:00
}
for base, aliases in alias_map.items():
for alias in aliases:
LOGOS[alias] = LOGOS[base]
2025-09-03 15:00:17 -04:00
LOG_FMT = (
"[%(asctime)s] "
"%(levelname)-8s "
"[%(name)s] "
"%(message)-70s "
"(%(filename)s:%(lineno)d)"
)
COLORS = {
"DEBUG": "\033[37m",
"INFO": "\033[32m",
"WARNING": "\033[33m",
"ERROR": "\033[31m",
"CRITICAL": "\033[41m",
"reset": "\033[0m",
}
class ColorFormatter(logging.Formatter):
def format(self, record) -> str:
color = COLORS.get(record.levelname, "")
levelname = record.levelname
record.levelname = f"{color}{levelname}{COLORS['reset']}"
formatted = super().format(record)
record.levelname = levelname
return formatted
def get_logger(name: str | None = None) -> logging.Logger:
if not name:
name = Path(__file__).stem
logger = logging.getLogger(name)
if not logger.hasHandlers():
handler = logging.StreamHandler()
formatter = ColorFormatter(LOG_FMT, datefmt="%Y-%m-%d | %H:%M:%S")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
return logger
2025-09-06 16:16:36 -04:00
def near_hr(dt: datetime) -> float:
return dt.replace(minute=0, second=0, microsecond=0).timestamp()
2025-09-09 13:34:16 -04:00
def is_fresh(
entry: dict,
nearest_hr: bool,
exp: int,
) -> bool:
2025-09-16 15:55:08 -04:00
ts: float | int = entry.get("timestamp", 31496400)
2025-09-09 13:34:16 -04:00
if nearest_hr:
ts = near_hr(datetime.fromtimestamp(ts))
return now.timestamp() - ts < exp
2025-09-05 10:37:22 -04:00
def load_cache(
2025-09-04 19:53:27 -04:00
file: Path,
2025-09-09 13:34:16 -04:00
exp: int | float,
2025-09-06 16:16:36 -04:00
nearest_hr: bool = False,
2025-09-09 13:34:16 -04:00
per_entry: bool = True,
2025-09-04 19:53:27 -04:00
) -> dict[str, dict[str, str | float]]:
try:
2025-09-09 13:34:16 -04:00
data: dict = json.loads(file.read_text(encoding="utf-8"))
2025-09-04 19:53:27 -04:00
except (FileNotFoundError, json.JSONDecodeError):
return {}
2025-09-09 13:34:16 -04:00
if per_entry:
return {k: v for k, v in data.items() if is_fresh(v, nearest_hr, exp)}
2025-09-16 15:55:08 -04:00
ts: float | int = data.get("timestamp", 31496400)
2025-09-09 13:34:16 -04:00
if nearest_hr:
ts = near_hr(datetime.fromtimestamp(ts))
return data if now.timestamp() - ts < exp else {}
2025-09-04 19:53:27 -04:00
2025-09-15 09:26:20 -04:00
def write_cache(file: Path, data: dict) -> None:
file.write_text(json.dumps(data, indent=2), encoding="utf-8")
2025-09-03 15:00:17 -04:00
async def safe_process_event(
2025-09-08 12:02:36 -04:00
fn: Callable,
2025-09-03 15:00:17 -04:00
url_num: int,
2025-09-08 12:02:36 -04:00
timeout: int | float = 20,
2025-09-03 15:00:17 -04:00
log: logging.Logger | None = None,
) -> Any | None:
if not log:
log = logging.getLogger(__name__)
task = asyncio.create_task(fn())
try:
return await asyncio.wait_for(task, timeout=timeout)
except asyncio.TimeoutError:
log.warning(f"URL {url_num}) Timed out after {timeout}s, skipping event")
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
except Exception as e:
log.debug(f"URL {url_num}) Ignore exception after timeout: {e}")
async def check_status(client: httpx.AsyncClient, url: str) -> bool:
try:
r = await client.get(url)
r.raise_for_status()
except Exception:
return False
return r.status_code == 200
2025-09-15 08:44:57 -04:00
async def get_base(client: httpx.AsyncClient, mirrors: list[str]) -> str | None:
2025-09-03 15:00:17 -04:00
tasks = [check_status(client, link) for link in mirrors]
results = await asyncio.gather(*tasks)
2025-09-15 08:44:57 -04:00
try:
return [url for url, ok in zip(mirrors, results) if ok][0]
except IndexError:
return
2025-09-04 19:53:27 -04:00
def capture_req(
req: Request,
captured: list[str],
got_one: asyncio.Event,
) -> None:
valid_m3u8 = re.compile(r"^(?!.*(amazonaws|knitcdn)).*\.m3u8")
if valid_m3u8.search(req.url):
captured.append(req.url)
got_one.set()
2025-09-11 14:55:53 -04:00
async def firefox(
playwright: Playwright, ignore_https_errors: bool = False
) -> tuple[Browser, BrowserContext]:
browser = await playwright.firefox.launch(headless=True)
context = await browser.new_context(
user_agent=UA,
viewport={"width": 1366, "height": 768},
device_scale_factor=1,
locale="en-US",
timezone_id="America/New_York",
color_scheme="dark",
permissions=["geolocation"],
extra_http_headers={
"Accept-Language": "en-US,en;q=0.9",
"Upgrade-Insecure-Requests": "1",
},
ignore_https_errors=ignore_https_errors,
)
await context.add_init_script(
"""
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en']
});
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4]
});
const elementDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight');
Object.defineProperty(HTMLDivElement.prototype, 'offsetHeight', {
...elementDescriptor,
get: function() {
if (this.id === 'modernizr') { return 24; }
return elementDescriptor.get.apply(this);
}
});
Object.defineProperty(window.screen, 'width', { get: () => 1366 });
Object.defineProperty(window.screen, 'height', { get: () => 768 });
const getParameter = WebGLRenderingContext.prototype. getParameter;
WebGLRenderingContext.prototype.getParameter = function (param) {
if (param === 37445) return "Intel Inc."; // UNMASKED_VENDOR_WEBGL
if (param === 37446) return "Intel Iris OpenGL Engine"; // UNMASKED_RENDERER_WEBGL
return getParameter.apply(this, [param]);
};
2025-09-17 17:42:35 -04:00
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.tagName === 'IFRAME' && node.hasAttribute('sandbox')) {
node.removeAttribute('sandbox');
}
});
});
});
observer.observe(document.documentElement, { childList: true, subtree: true });
2025-09-11 14:55:53 -04:00
"""
)
return browser, context