Compare commits

..

No commits in common. "cdb7d9f94d139f21ca25c6161b179a9014b8a245" and "e9438eab81a42224dc023ff07c311ba2d2e1c337" have entirely different histories.

12 changed files with 114768 additions and 116435 deletions

File diff suppressed because it is too large Load diff

225509
M3U8/TV.xml

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -17,11 +17,9 @@ from scrapers import (
roxie, roxie,
shark, shark,
sport9, sport9,
streambtw,
streamcenter, streamcenter,
streamhub, streamhub,
streamsgate, streamsgate,
timstreams,
totalsportek, totalsportek,
tvapp, tvapp,
volokit, volokit,
@ -72,8 +70,7 @@ async def main() -> None:
asyncio.create_task(sport9.scrape(xtrnl_brwsr)), asyncio.create_task(sport9.scrape(xtrnl_brwsr)),
asyncio.create_task(streamcenter.scrape(hdl_brwsr)), asyncio.create_task(streamcenter.scrape(hdl_brwsr)),
# asyncio.create_task(streamhub.scrape(xtrnl_brwsr)), # asyncio.create_task(streamhub.scrape(xtrnl_brwsr)),
asyncio.create_task(streamsgate.scrape(xtrnl_brwsr)), asyncio.create_task(streamsgate.scrape(hdl_brwsr)),
asyncio.create_task(timstreams.scrape(xtrnl_brwsr)),
] ]
httpx_tasks = [ httpx_tasks = [
@ -82,7 +79,6 @@ async def main() -> None:
asyncio.create_task(ovogoal.scrape()), asyncio.create_task(ovogoal.scrape()),
asyncio.create_task(pawa.scrape()), asyncio.create_task(pawa.scrape()),
asyncio.create_task(shark.scrape()), asyncio.create_task(shark.scrape()),
asyncio.create_task(streambtw.scrape()),
asyncio.create_task(totalsportek.scrape()), asyncio.create_task(totalsportek.scrape()),
asyncio.create_task(tvapp.scrape()), asyncio.create_task(tvapp.scrape()),
asyncio.create_task(volokit.scrape()), asyncio.create_task(volokit.scrape()),
@ -116,11 +112,9 @@ async def main() -> None:
| roxie.urls | roxie.urls
| shark.urls | shark.urls
| sport9.urls | sport9.urls
| streambtw.urls
| streamcenter.urls | streamcenter.urls
| streamhub.urls | streamhub.urls
| streamsgate.urls | streamsgate.urls
| timstreams.urls
| totalsportek.urls | totalsportek.urls
| tvapp.urls | tvapp.urls
| volokit.urls | volokit.urls

View file

@ -1,4 +1,3 @@
import re
from functools import partial from functools import partial
from playwright.async_api import Browser from playwright.async_api import Browser
@ -15,7 +14,7 @@ CACHE_FILE = Cache(TAG, exp=10_800)
API_FILE = Cache(f"{TAG}-api", exp=19_800) API_FILE = Cache(f"{TAG}-api", exp=19_800)
API_MIRRORS = [ MIRRORS = [
"https://api.ppv.to/api/streams", "https://api.ppv.to/api/streams",
"https://api.ppv.cx/api/streams", "https://api.ppv.cx/api/streams",
"https://api.ppv.sh/api/streams", "https://api.ppv.sh/api/streams",
@ -23,12 +22,6 @@ API_MIRRORS = [
] ]
def fix_url(s: str) -> str:
pattern = re.compile(r"index\.m3u8$", re.I)
return pattern.sub(r"tracks-v1a1/mono.ts.m3u8", s)
async def get_events(url: str, cached_keys: list[str]) -> list[dict[str, str]]: async def get_events(url: str, cached_keys: list[str]) -> list[dict[str, str]]:
now = Time.clean(Time.now()) now = Time.clean(Time.now())
@ -97,16 +90,16 @@ async def scrape(browser: Browser) -> None:
log.info(f"Loaded {cached_count} event(s) from cache") log.info(f"Loaded {cached_count} event(s) from cache")
if not (api_url := await network.get_base(API_MIRRORS)): if not (base_url := await network.get_base(MIRRORS)):
log.warning("No working PPV mirrors") log.warning("No working PPV mirrors")
CACHE_FILE.write(cached_urls) CACHE_FILE.write(cached_urls)
return return
log.info(f'Scraping from "{api_url}"') log.info(f'Scraping from "{base_url}"')
events = await get_events(api_url, cached_urls.keys()) events = await get_events(base_url, cached_urls.keys())
if events: if events:
log.info(f"Processing {len(events)} new URL(s)") log.info(f"Processing {len(events)} new URL(s)")
@ -155,8 +148,6 @@ async def scrape(browser: Browser) -> None:
if url: if url:
valid_count += 1 valid_count += 1
entry["url"] = fix_url(url)
urls[key] = entry urls[key] = entry
if new_count := valid_count - cached_count: if new_count := valid_count - cached_count:

View file

@ -31,11 +31,9 @@ async def process_event(url: str, url_num: int) -> str | None:
return return
pattern = re.compile(r"playlist\.m3u8\?.*$", re.I)
log.info(f"URL {url_num}) Captured M3U8") log.info(f"URL {url_num}) Captured M3U8")
return pattern.sub(r"chunks.m3u8", urls[0]) return urls[0]
async def refresh_html_cache(now_ts: float) -> dict[str, dict[str, str | float]]: async def refresh_html_cache(now_ts: float) -> dict[str, dict[str, str | float]]:

View file

@ -1,147 +0,0 @@
import base64
import re
from functools import partial
from urllib.parse import urljoin
from .utils import Cache, Time, get_logger, leagues, network
log = get_logger(__name__)
urls: dict[str, dict[str, str | float]] = {}
TAG = "STRMBTW"
CACHE_FILE = Cache(TAG, exp=3_600)
API_FILE = Cache(f"{TAG}-api", exp=19_800)
BASE_URL = "https://streambtw.com"
def fix_league(s: str) -> str:
pattern = re.compile(r"^\w*-\w*", re.I)
return " ".join(s.split("-")) if pattern.search(s) else s
async def process_event(url: str, url_num: int) -> str | None:
if not (html_data := await network.request(url, log=log)):
return
valid_m3u8 = re.compile(r'var\s+(\w+)\s*=\s*"([^"]*)"', re.I)
if not (match := valid_m3u8.search(html_data.text)):
log.info(f"URL {url_num}) No M3U8 found")
return
stream_link: str = match[2]
if not stream_link.startswith("http"):
stream_link = base64.b64decode(stream_link).decode("utf-8")
log.info(f"URL {url_num}) Captured M3U8")
return stream_link
async def get_events() -> list[dict[str, str]]:
now = Time.clean(Time.now())
if not (api_data := API_FILE.load(per_entry=False)):
log.info("Refreshing API cache")
api_data = {"timestamp": now.timestamp()}
if r := await network.request(
urljoin(BASE_URL, "public/api.php"),
log=log,
params={"action": "get"},
):
api_data: dict = r.json()
api_data["timestamp"] = now.timestamp()
API_FILE.write(api_data)
events = []
if last_update := api_data.get("updated_at"):
last_update_dt = Time.from_str(last_update, timezone="UTC")
if last_update_dt.date() != now.date():
return events
for info in api_data.get("groups", []):
if not (sport := info["title"]):
sport = "Live Event"
if items := info.get("items"):
for event in items:
event_name: str = event["title"]
if not (link := event.get("url")):
continue
events.append(
{
"sport": fix_league(sport),
"event": event_name,
"link": link,
}
)
return events
async def scrape() -> None:
if cached := CACHE_FILE.load():
urls.update(cached)
log.info(f"Loaded {len(urls)} event(s) from cache")
return
log.info(f'Scraping from "{BASE_URL}"')
events = await get_events()
if events:
log.info(f"Processing {len(events)} new URL(s)")
now = Time.clean(Time.now())
for i, ev in enumerate(events, start=1):
handler = partial(
process_event,
url=(link := ev["link"]),
url_num=i,
)
url = await network.safe_process(
handler,
url_num=i,
semaphore=network.HTTP_S,
log=log,
)
if url:
sport, event = ev["sport"], ev["event"]
key = f"[{sport}] {event} ({TAG})"
tvg_id, logo = leagues.get_tvg_info(sport, event)
entry = {
"url": url,
"logo": logo,
"base": link,
"timestamp": now.timestamp(),
"id": tvg_id or "Live.Event.us",
"link": link,
}
urls[key] = entry
log.info(f"Collected {len(urls)} event(s)")
CACHE_FILE.write(urls)

View file

@ -92,11 +92,9 @@ async def get_events(cached_keys: list[str]) -> list[dict[str, str]]:
async def scrape(browser: Browser) -> None: async def scrape(browser: Browser) -> None:
cached_urls = CACHE_FILE.load() cached_urls = CACHE_FILE.load()
valid_urls = {k: v for k, v in cached_urls.items() if v["url"]} cached_count = len(cached_urls)
valid_count = cached_count = len(valid_urls) urls.update(cached_urls)
urls.update(valid_urls)
log.info(f"Loaded {cached_count} event(s) from cache") log.info(f"Loaded {cached_count} event(s) from cache")
@ -107,7 +105,7 @@ async def scrape(browser: Browser) -> None:
if events: if events:
log.info(f"Processing {len(events)} new URL(s)") log.info(f"Processing {len(events)} new URL(s)")
async with network.event_context(browser) as context: async with network.event_context(browser, stealth=False) as context:
for i, ev in enumerate(events, start=1): for i, ev in enumerate(events, start=1):
async with network.event_page(context) as page: async with network.event_page(context) as page:
handler = partial( handler = partial(
@ -125,6 +123,7 @@ async def scrape(browser: Browser) -> None:
log=log, log=log,
) )
if url:
sport, event, ts = ( sport, event, ts = (
ev["sport"], ev["sport"],
ev["event"], ev["event"],
@ -144,16 +143,9 @@ async def scrape(browser: Browser) -> None:
"link": link, "link": link,
} }
cached_urls[key] = entry urls[key] = cached_urls[key] = entry
if url: if new_count := len(cached_urls) - cached_count:
valid_count += 1
entry["url"] = url.split("?")[0]
urls[key] = entry
if new_count := valid_count - cached_count:
log.info(f"Collected and cached {new_count} new event(s)") log.info(f"Collected and cached {new_count} new event(s)")
else: else:

View file

@ -123,11 +123,9 @@ async def get_events(cached_keys: list[str]) -> list[dict[str, str]]:
async def scrape(browser: Browser) -> None: async def scrape(browser: Browser) -> None:
cached_urls = CACHE_FILE.load() cached_urls = CACHE_FILE.load()
valid_urls = {k: v for k, v in cached_urls.items() if v["url"]} cached_count = len(cached_urls)
valid_count = cached_count = len(valid_urls) urls.update(cached_urls)
urls.update(valid_urls)
log.info(f"Loaded {cached_count} event(s) from cache") log.info(f"Loaded {cached_count} event(s) from cache")
@ -156,6 +154,7 @@ async def scrape(browser: Browser) -> None:
log=log, log=log,
) )
if url:
sport, event, ts = ( sport, event, ts = (
ev["sport"], ev["sport"],
ev["event"], ev["event"],
@ -175,16 +174,9 @@ async def scrape(browser: Browser) -> None:
"link": link, "link": link,
} }
cached_urls[key] = entry urls[key] = cached_urls[key] = entry
if url: if new_count := len(cached_urls) - cached_count:
valid_count += 1
entry["url"] = url.split("&e")[0]
urls[key] = entry
if new_count := valid_count - cached_count:
log.info(f"Collected and cached {new_count} new event(s)") log.info(f"Collected and cached {new_count} new event(s)")
else: else:

View file

@ -1,177 +0,0 @@
from functools import partial
from typing import Any
from urllib.parse import urljoin
from playwright.async_api import Browser
from .utils import Cache, Time, get_logger, leagues, network
log = get_logger(__name__)
urls: dict[str, dict[str, str | float]] = {}
TAG = "TIMSTRMS"
CACHE_FILE = Cache(TAG, exp=10_800)
API_FILE = Cache(f"{TAG}-api", exp=19_800)
API_URL = "https://stra.viaplus.site/main"
BASE_URL = "https://timstreams.fit"
SPORT_GENRES = {
1: "Soccer",
2: "Motorsport",
3: "MMA",
4: "Fight",
5: "Boxing",
6: "Wrestling",
7: "Basketball",
# 8: "American Football",
9: "Baseball",
10: "Tennis",
11: "Hockey",
# 12: "Darts",
# 13: "Cricket",
# 14: "Cycling",
# 15: "Rugby",
# 16: "Live Shows",
# 17: "Other",
}
async def get_events(cached_keys: list[str]) -> list[dict[str, str]]:
now = Time.clean(Time.now())
if not (api_data := API_FILE.load(per_entry=False, index=-1)):
log.info("Refreshing API cache")
api_data = [{"timestamp": now.timestamp()}]
if r := await network.request(API_URL, log=log):
api_data: list[dict] = r.json()
api_data[-1]["timestamp"] = now.timestamp()
API_FILE.write(api_data)
events = []
start_dt = now.delta(minutes=-30)
end_dt = now.delta(minutes=30)
for info in api_data:
if not (category := info.get("category")) or category != "Events":
continue
stream_events: list[dict[str, Any]] = info["events"]
for ev in stream_events:
if (genre := ev["genre"]) not in SPORT_GENRES:
continue
event_dt = Time.from_str(ev["time"], timezone="EST")
if not start_dt <= event_dt <= end_dt:
continue
name: str = ev["name"]
url_id: str = ev["URL"]
logo: str | None = ev.get("logo")
sport = SPORT_GENRES[genre]
if f"[{sport}] {name} ({TAG})" in cached_keys:
continue
if not (streams := ev["streams"]) or not (url := streams[0].get("url")):
continue
events.append(
{
"sport": sport,
"event": name,
"link": urljoin(BASE_URL, f"watch?id={url_id}"),
"ref": url,
"logo": logo,
"timestamp": event_dt.timestamp(),
}
)
return events
async def scrape(browser: Browser) -> None:
cached_urls = CACHE_FILE.load()
valid_urls = {k: v for k, v in cached_urls.items() if v["url"]}
valid_count = cached_count = len(valid_urls)
urls.update(valid_urls)
log.info(f"Loaded {cached_count} event(s) from cache")
log.info(f'Scraping from "{BASE_URL}"')
events = await get_events(cached_urls.keys())
if events:
log.info(f"Processing {len(events)} new URL(s)")
async with network.event_context(browser, stealth=False) as context:
for i, ev in enumerate(events, start=1):
async with network.event_page(context) as page:
handler = partial(
network.process_event,
url=(link := ev["link"]),
url_num=i,
page=page,
log=log,
)
url = await network.safe_process(
handler,
url_num=i,
semaphore=network.PW_S,
log=log,
)
sport, event, logo, ref, ts = (
ev["sport"],
ev["event"],
ev["logo"],
ev["ref"],
ev["timestamp"],
)
key = f"[{sport}] {event} ({TAG})"
tvg_id, pic = leagues.get_tvg_info(sport, event)
entry = {
"url": url,
"logo": logo or pic,
"base": ref,
"timestamp": ts,
"id": tvg_id or "Live.Event.us",
"link": link,
}
cached_urls[key] = entry
if url:
valid_count += 1
urls[key] = entry
if new_count := valid_count - cached_count:
log.info(f"Collected and cached {new_count} new event(s)")
else:
log.info("No new events found")
CACHE_FILE.write(cached_urls)

View file

@ -15,7 +15,7 @@ TAG = "TOTALSPRTK"
CACHE_FILE = Cache(TAG, exp=28_800) CACHE_FILE = Cache(TAG, exp=28_800)
BASE_URL = "https://live3.totalsportekarmy.com" BASE_URL = "https://live3.totalsportek777.com"
def fix_txt(s: str) -> str: def fix_txt(s: str) -> str:

View file

@ -1,14 +1,13 @@
## Base Log @ 2026-03-01 08:53 UTC ## Base Log @ 2026-02-28 08:50 UTC
### ✅ Working Streams: 156<br>❌ Dead Streams: 5 ### ✅ Working Streams: 157<br>❌ Dead Streams: 4
| Channel | Error (Code) | Link | | Channel | Error (Code) | Link |
| ------- | ------------ | ---- | | ------- | ------------ | ---- |
| Disney XD | HTTP Error (000) | `http://hardcoremedia.xyz/live/rabdsbmz/3731346838/130092.ts` | | BET | HTTP Error (404) | `http://212.102.60.231/BET/index.m3u8` |
| Disney | HTTP Error (000) | `http://hardcoremedia.xyz/live/rabdsbmz/3731346838/257087.ts` | | Disney | HTTP Error (000) | `http://hardcoremedia.xyz/live/rabdsbmz/3731346838/257087.ts` |
| FYI TV | HTTP Error (000) | `http://hardcoremedia.xyz/live/rabdsbmz/3731346838/130105.ts` | | FYI TV | HTTP Error (000) | `http://hardcoremedia.xyz/live/rabdsbmz/3731346838/130105.ts` |
| Golf Channel | HTTP Error (000) | `http://hardcoremedia.xyz/live/rabdsbmz/3731346838/258721.ts` | | Golf Channel | HTTP Error (000) | `http://hardcoremedia.xyz/live/rabdsbmz/3731346838/258721.ts` |
| NBC Sports NOW | HTTP Error (403) | `https://starshare.st/live/P4B9TB9xR8/humongous2tonight/1001.ts` |
--- ---
#### Base Channels URL #### Base Channels URL
``` ```