Fix for M3U8 Hotstar

This commit is contained in:
chu23465 2025-04-16 17:20:32 +05:30
parent 5b88624051
commit a8edede94b
5 changed files with 125 additions and 112 deletions

View File

@ -71,7 +71,7 @@ class Track:
ISM = 4 # https://bitmovin.com/blog/microsoft-smooth-streaming-mss/
def __init__(self, id_, source, url, codec, language=None, descriptor=Descriptor.URL,
needs_proxy=False, needs_repack=False, encrypted=False, psshWV=None, psshPR=None, note=None, kid=None, key=None, extra=None):
needs_proxy=False, needs_repack=False, encrypted=False, psshWV=None, psshPR=None, note=None, kid=None, key=None, extra=None, original_url=None):
self.id = id_
self.source = source
self.url = url
@ -94,6 +94,8 @@ class Track:
# extra data
self.extra = extra or {} # allow anything for extra, but default to a dict
self.original_url = original_url
# should only be set internally
self._location = None

View File

@ -196,6 +196,7 @@ def parse(*, url=None, data=None, source, session=None, downloader=None):
tracks.append(VideoTrack(
id_=track_id,
source=source,
original_url=url,
url=url,
# metadata
codec=(codec or "").split(".")[0],

View File

@ -8,115 +8,118 @@ from vinetrimmer.vendor.pymp4.parser import Box
def parse(master, source=None):
"""
Convert a Variant Playlist M3U8 document to a Tracks object with Video, Audio and
Subtitle Track objects. This is not an M3U8 parser, use https://github.com/globocom/m3u8
to parse, and then feed the parsed M3U8 object.
"""
Convert a Variant Playlist M3U8 document to a Tracks object with Video, Audio and
Subtitle Track objects. This is not an M3U8 parser, use https://github.com/globocom/m3u8
to parse, and then feed the parsed M3U8 object.
:param master: M3U8 object of the `m3u8` project: https://github.com/globocom/m3u8
:param source: Source tag for the returned tracks.
:param master: M3U8 object of the `m3u8` project: https://github.com/globocom/m3u8
:param source: Source tag for the returned tracks.
The resulting Track objects' URL will be to another M3U8 file, but this time to an
actual media stream and not to a variant playlist. The m3u8 downloader code will take
care of that, as the tracks downloader will be set to `M3U8`.
The resulting Track objects' URL will be to another M3U8 file, but this time to an
actual media stream and not to a variant playlist. The m3u8 downloader code will take
care of that, as the tracks downloader will be set to `M3U8`.
Don't forget to manually handle the addition of any needed or extra information or values.
Like `encrypted`, `pssh`, `hdr10`, `dv`, e.t.c. Essentially anything that is per-service
should be looked at. Some of these values like `pssh` and `dv` will try to be set automatically
if possible but if you definitely have the values in the service, then set them.
Subtitle Codec will default to vtt as it has no codec information.
Don't forget to manually handle the addition of any needed or extra information or values.
Like `encrypted`, `pssh`, `hdr10`, `dv`, e.t.c. Essentially anything that is per-service
should be looked at. Some of these values like `pssh` and `dv` will try to be set automatically
if possible but if you definitely have the values in the service, then set them.
Subtitle Codec will default to vtt as it has no codec information.
Example:
tracks = Tracks.from_m3u8(m3u8.load(url))
# check the m3u8 project for more info and ways to parse m3u8 documents
"""
if not master.is_variant:
raise ValueError("Tracks.from_m3u8: Expected a Variant Playlist M3U8 document...")
Example:
tracks = Tracks.from_m3u8(m3u8.load(url))
# check the m3u8 project for more info and ways to parse m3u8 documents
"""
if not master.is_variant:
raise ValueError("Tracks.from_m3u8: Expected a Variant Playlist M3U8 document...")
# get pssh if available
# uses master.data.session_keys instead of master.keys as master.keys is ONLY EXT-X-KEYS and
# doesn't include EXT-X-SESSION-KEYS which is whats used for variant playlist M3U8.
keys = [x.uri for x in master.session_keys if x.keyformat.lower() == "com.microsoft.playready"]
psshPR = keys[0].split(",")[-1] if keys else None
# get pssh if available
# uses master.data.session_keys instead of master.keys as master.keys is ONLY EXT-X-KEYS and
# doesn't include EXT-X-SESSION-KEYS which is whats used for variant playlist M3U8.
keys = [x.uri for x in master.session_keys if x.keyformat.lower() == "com.microsoft.playready"]
psshPR = keys[0].split(",")[-1] if keys else None
widevine_keys = [x.uri for x in master.session_keys if x.keyformat.lower() == "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"]
psshWV = widevine_keys[0].split(",")[-1] if widevine_keys else None
# if pssh:
# pssh = base64.b64decode(pssh)
# # noinspection PyBroadException
# try:
# pssh = Box.parse(pssh)
# except Exception:
# pssh = Box.parse(Box.build(dict(
# type=b"pssh",
# version=0, # can only assume version & flag are 0
# flags=0,
# system_ID=Cdm.uuid,
# init_data=pssh
# )))
widevine_keys = [x.uri for x in master.session_keys if x.keyformat.lower() == "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"]
psshWV = widevine_keys[0].split(",")[-1] if widevine_keys else None
# if pssh:
# pssh = base64.b64decode(pssh)
# # noinspection PyBroadException
# try:
# pssh = Box.parse(pssh)
# except Exception:
# pssh = Box.parse(Box.build(dict(
# type=b"pssh",
# version=0, # can only assume version & flag are 0
# flags=0,
# system_ID=Cdm.uuid,
# init_data=pssh
# )))
return Tracks(
# VIDEO
[VideoTrack(
id_=md5(str(x).encode()).hexdigest()[0:7], # 7 chars only for filename length
source=source,
url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri,
# metadata
codec=x.stream_info.codecs.split(",")[0].split(".")[0], # first codec may not be for the video
language=None, # playlists don't state the language, fallback must be used
bitrate=x.stream_info.average_bandwidth or x.stream_info.bandwidth,
width=x.stream_info.resolution[0],
height=x.stream_info.resolution[1],
fps=x.stream_info.frame_rate,
hdr10=(x.stream_info.codecs.split(".")[0] not in ("dvhe", "dvh1")
and (x.stream_info.video_range or "SDR").strip('"') != "SDR"),
hlg=False, # TODO: Can we get this from the manifest.xml?
dv=x.stream_info.codecs.split(".")[0] in ("dvhe", "dvh1"),
# switches/options
descriptor=Track.Descriptor.M3U,
# decryption
encrypted=bool(master.keys or master.session_keys),
psshWV=psshWV,
psshPR=psshPR,
# extra
extra=x
) for x in master.playlists],
# AUDIO
[AudioTrack(
id_=md5(str(x).encode()).hexdigest()[0:6],
source=source,
url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri,
# metadata
codec=x.group_id.replace("audio-", "").split("-")[0].split(".")[0],
language=x.language,
bitrate=0, # TODO: M3U doesn't seem to state bitrate?
channels=x.channels,
atmos=(x.channels or "").endswith("/JOC"),
descriptive="public.accessibility.describes-video" in (x.characteristics or ""),
# switches/options
descriptor=Track.Descriptor.M3U,
# decryption
encrypted=False, # don't know for sure if encrypted
psshWV=psshWV,
psshPR=psshPR,
# extra
extra=x
) for x in master.media if x.type == "AUDIO" and x.uri],
# SUBTITLES
[TextTrack(
id_=md5(str(x).encode()).hexdigest()[0:6],
source=source,
url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri,
# metadata
codec="vtt", # assuming VTT, codec info isn't shown
language=x.language,
forced=x.forced == "YES",
sdh="public.accessibility.describes-music-and-sound" in (x.characteristics or ""),
# switches/options
descriptor=Track.Descriptor.M3U,
# extra
extra=x
) for x in master.media if x.type == "SUBTITLES"]
)
return Tracks(
# VIDEO
[VideoTrack(
id_=md5(str(x).encode()).hexdigest()[0:7], # 7 chars only for filename length
source=source,
original_url=x.base_uri + x.uri,
url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri,
# metadata
codec=x.stream_info.codecs.split(",")[0].split(".")[0], # first codec may not be for the video
language=None, # playlists don't state the language, fallback must be used
bitrate=x.stream_info.average_bandwidth or x.stream_info.bandwidth,
width=x.stream_info.resolution[0],
height=x.stream_info.resolution[1],
fps=x.stream_info.frame_rate,
hdr10=(x.stream_info.codecs.split(".")[0] not in ("dvhe", "dvh1")
and (x.stream_info.video_range or "SDR").strip('"') != "SDR"),
hlg=False, # TODO: Can we get this from the manifest.xml?
dv=x.stream_info.codecs.split(".")[0] in ("dvhe", "dvh1"),
# switches/options
descriptor=Track.Descriptor.M3U,
# decryption
encrypted=bool(master.keys or master.session_keys),
psshWV=psshWV,
psshPR=psshPR,
# extra
extra=x
) for x in master.playlists],
# AUDIO
[AudioTrack(
id_=md5(str(x).encode()).hexdigest()[0:6],
source=source,
original_url=x.base_uri + x.uri,
url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri,
# metadata
codec=x.group_id.replace("audio-", "").split("-")[0].split(".")[0],
language=x.language,
bitrate=0, # TODO: M3U doesn't seem to state bitrate?
channels=x.channels,
atmos=(x.channels or "").endswith("/JOC"),
descriptive="public.accessibility.describes-video" in (x.characteristics or ""),
# switches/options
descriptor=Track.Descriptor.M3U,
# decryption
encrypted=False, # don't know for sure if encrypted
psshWV=psshWV,
psshPR=psshPR,
# extra
extra=x
) for x in master.media if x.type == "AUDIO" and x.uri],
# SUBTITLES
[TextTrack(
id_=md5(str(x).encode()).hexdigest()[0:6],
source=source,
original_url=x.base_uri + x.uri,
url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri,
# metadata
codec="vtt", # assuming VTT, codec info isn't shown
language=x.language,
forced=x.forced == "YES",
sdh="public.accessibility.describes-music-and-sound" in (x.characteristics or ""),
# switches/options
descriptor=Track.Descriptor.M3U,
# extra
extra=x
) for x in master.media if x.type == "SUBTITLES"]
)

View File

@ -287,7 +287,8 @@ def parse(*, url=None, data=None, source, session=None, downloader=None):
tracks.append(VideoTrack(
id_=track_id,
source=source,
url=url if source == "HS" else track_url,
original_url=url,
url=track_url,
# metadata
codec=(codecs or "").split(".")[0],
language=track_lang,
@ -324,7 +325,8 @@ def parse(*, url=None, data=None, source, session=None, downloader=None):
tracks.append(AudioTrack(
id_=track_id,
source=source,
url=url if source == "HS" else track_url,
original_url=url,
url=track_url,
# metadata
codec=(codecs or "").split(".")[0],
language=track_lang,
@ -375,6 +377,7 @@ def parse(*, url=None, data=None, source, session=None, downloader=None):
tracks.append(TextTrack(
id_=track_id,
source=source,
original_url=url,
url=track_url,
# metadata
codec=(codecs or "").split(".")[0],
@ -390,6 +393,7 @@ def parse(*, url=None, data=None, source, session=None, downloader=None):
tracks.append(TextTrack(
id_=track_id,
source=source,
original_url=url,
url=track_url,
# metadata
codec=(codecs or "").split(".")[0],

View File

@ -248,8 +248,7 @@ async def m3u8dl(uri, out, track, headers=None, proxy=None):
ffmpeg_binary = shutil.which("ffmpeg") or "/usr/bin/ffmpeg"
arguments = [
executable,
uri,
**["--max-speed", "12M"] if "akamai" in uri else "",
track.original_url or uri,
"--save-dir", f'"{os.path.dirname(out)}"',
"--tmp-dir", f'"{os.path.dirname(out)}"',
"--save-name", f'"{os.path.basename(out).replace(".mp4", "")}"',
@ -263,14 +262,18 @@ async def m3u8dl(uri, out, track, headers=None, proxy=None):
if headers and track.source == "HS":
arguments.extend(["--header", f'"Cookie:{headers["cookie"].replace(" ", "")}"'])
#for k,v in headers.items():
if "akamai" in uri:
arguments.append("--max-speed")
arguments.append("12M")
if proxy:
arguments.extend(["--custom-proxy", proxy])
if not ("linux" in platform):
arguments.extend(["--http-request-timeout", "8"])
if track.__class__.__name__ == "VideoTrack":
if track.height:
from vinetrimmer.objects.tracks import Track
if track.height and not (track.descriptor == Track.Descriptor.M3U):
arguments.extend([
"-sv", f"res='{track.height}*':codec='{track.codec}':for=best"
])