mirror of
https://github.com/Free-TV/IPTV.git
synced 2026-09-25 03:21:20 +02:00
Restore official geo-blocked RAI streams alongside the CloudFront mirror
Fixes #1179. PR #1166 moved Rai 1, Rai 2, Rai 3, Rai Premium, Rai Storia and Rai Scuola from the official mediapolis.rai.it relinker to a third-party CloudFront mirror, because the relinker had become Italy-IP-gated and was failing for everyone else (including the CI checker). That mirror works for casual viewing, but as #1179 reports, it goes black during broadcasts where RAI only holds domestic rights (e.g. EuroVolley) - the official stream keeps working correctly for Italian-IP viewers through exactly those broadcasts, because it's RAI's own domestic feed, not a re-mirror of it. Rather than trade one gap for the other, add the official mediapolis URL back as a second, Ⓖ-marked entry for each of the six channels - the same "CDN mirror as the general-access entry, official relinker as the geo-gated alternative" split this file already uses for every other Rai channel (Rai 4, Rai 5, Rai Movie, Rai Sport, etc., all still on mediapolis.rai.it + Ⓖ). Viewers with an Italian IP get the more correct source; everyone else keeps the mirror that already works for them. Also fixes a real gap in check_channels.py found while verifying this: mediapolis.rai.it's relinker doesn't refuse an out-of-territory request with an HTTP error - it answers 200 with a real MP4, but the file is always the same "video_no_available.mp4" decoy regardless of which channel was asked for. The checker had no way to tell that apart from a working stream failing its playlist-header check, so every mediapolis.rai.it entry (the pre-existing Rai 4/5/Movie/Sport ones included) was scored `dead` instead of `blocked`, right down to the newly-fixed pr_check.yml workflow that now hard-fails a PR over a `dead`-scored touched channel. Added a check for this specific decoy redirect target, classified as `blocked` like any other geo-refusal. Regenerated playlist.m3u8 and playlists/playlist_italy.m3u8 via make_playlist.py. pylint check_channels.py: 10.00/10. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Enus8N247jHM5r5Som5JuU
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d9bb826b0b
commit
72b7c7e076
+25
-3
@@ -135,6 +135,29 @@ def looks_like_a_raw_stream(content_type):
|
||||
return content_type.lower().startswith(STREAM_CONTENT_TYPES)
|
||||
|
||||
|
||||
# some geo-gated broadcasters (RAI's relinker is the known case) answer an
|
||||
# out-of-territory request with a plain HTTP 200 and a real video file - not an
|
||||
# error, a decoy: a short "this content is not available in your country" clip.
|
||||
# A player never notices anything is wrong; our probe would call it alive. The
|
||||
# redirect chain gives it away: it always lands on the same well-known asset,
|
||||
# regardless of which channel was asked for.
|
||||
GEOBLOCK_PLACEHOLDER_MARKERS = ("video_no_available.mp4",)
|
||||
|
||||
|
||||
def is_geoblock_placeholder(final_url):
|
||||
"""Return True if `final_url` is a known geo-block decoy, not a real stream."""
|
||||
return any(marker in final_url for marker in GEOBLOCK_PLACEHOLDER_MARKERS)
|
||||
|
||||
|
||||
def classify_response(final_url, content_type, head):
|
||||
"""Turn a completed HTTP response into an outcome: OK, REFUSED, or GONE."""
|
||||
if is_geoblock_placeholder(final_url):
|
||||
return REFUSED
|
||||
if looks_like_a_raw_stream(content_type):
|
||||
return OK
|
||||
return OK if looks_like_a_playlist(head) else GONE
|
||||
|
||||
|
||||
def probe(url, timeout):
|
||||
"""Open `url` once and report what the server did."""
|
||||
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
@@ -144,6 +167,7 @@ def probe(url, timeout):
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout, context=context) as response:
|
||||
final_url = response.geturl()
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
head = response.read(3000).decode("utf-8", "ignore")
|
||||
except urllib.error.HTTPError as error:
|
||||
@@ -157,9 +181,7 @@ def probe(url, timeout):
|
||||
# response and then hung up mid-chunk (IncompleteRead) and similar
|
||||
# low-level protocol violations - a broken connection, not a bad URL
|
||||
return UNREACHABLE
|
||||
if looks_like_a_raw_stream(content_type):
|
||||
return OK
|
||||
return OK if looks_like_a_playlist(head) else GONE
|
||||
return classify_response(final_url, content_type, head)
|
||||
|
||||
|
||||
def ffprobe_finds_a_stream(url):
|
||||
|
||||
Reference in New Issue
Block a user