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/ ISM = 4 # https://bitmovin.com/blog/microsoft-smooth-streaming-mss/
def __init__(self, id_, source, url, codec, language=None, descriptor=Descriptor.URL, 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.id = id_
self.source = source self.source = source
self.url = url self.url = url
@ -94,6 +94,8 @@ class Track:
# extra data # extra data
self.extra = extra or {} # allow anything for extra, but default to a dict self.extra = extra or {} # allow anything for extra, but default to a dict
self.original_url = original_url
# should only be set internally # should only be set internally
self._location = None self._location = None

View File

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

View File

@ -8,115 +8,118 @@ from vinetrimmer.vendor.pymp4.parser import Box
def parse(master, source=None): def parse(master, source=None):
""" """
Convert a Variant Playlist M3U8 document to a Tracks object with Video, Audio and 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 Subtitle Track objects. This is not an M3U8 parser, use https://github.com/globocom/m3u8
to parse, and then feed the parsed M3U8 object. to parse, and then feed the parsed M3U8 object.
:param master: M3U8 object of the `m3u8` project: https://github.com/globocom/m3u8 :param master: M3U8 object of the `m3u8` project: https://github.com/globocom/m3u8
:param source: Source tag for the returned tracks. :param source: Source tag for the returned tracks.
The resulting Track objects' URL will be to another M3U8 file, but this time to an 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 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`. 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. 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 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 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. 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. Subtitle Codec will default to vtt as it has no codec information.
Example: Example:
tracks = Tracks.from_m3u8(m3u8.load(url)) tracks = Tracks.from_m3u8(m3u8.load(url))
# check the m3u8 project for more info and ways to parse m3u8 documents # check the m3u8 project for more info and ways to parse m3u8 documents
""" """
if not master.is_variant: if not master.is_variant:
raise ValueError("Tracks.from_m3u8: Expected a Variant Playlist M3U8 document...") raise ValueError("Tracks.from_m3u8: Expected a Variant Playlist M3U8 document...")
# get pssh if available # get pssh if available
# uses master.data.session_keys instead of master.keys as master.keys is ONLY EXT-X-KEYS and # 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. # 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"] 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 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"] 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 psshWV = widevine_keys[0].split(",")[-1] if widevine_keys else None
# if pssh: # if pssh:
# pssh = base64.b64decode(pssh) # pssh = base64.b64decode(pssh)
# # noinspection PyBroadException # # noinspection PyBroadException
# try: # try:
# pssh = Box.parse(pssh) # pssh = Box.parse(pssh)
# except Exception: # except Exception:
# pssh = Box.parse(Box.build(dict( # pssh = Box.parse(Box.build(dict(
# type=b"pssh", # type=b"pssh",
# version=0, # can only assume version & flag are 0 # version=0, # can only assume version & flag are 0
# flags=0, # flags=0,
# system_ID=Cdm.uuid, # system_ID=Cdm.uuid,
# init_data=pssh # init_data=pssh
# ))) # )))
return Tracks( return Tracks(
# VIDEO # VIDEO
[VideoTrack( [VideoTrack(
id_=md5(str(x).encode()).hexdigest()[0:7], # 7 chars only for filename length id_=md5(str(x).encode()).hexdigest()[0:7], # 7 chars only for filename length
source=source, source=source,
url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri, original_url=x.base_uri + x.uri,
# metadata url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri,
codec=x.stream_info.codecs.split(",")[0].split(".")[0], # first codec may not be for the video # metadata
language=None, # playlists don't state the language, fallback must be used codec=x.stream_info.codecs.split(",")[0].split(".")[0], # first codec may not be for the video
bitrate=x.stream_info.average_bandwidth or x.stream_info.bandwidth, language=None, # playlists don't state the language, fallback must be used
width=x.stream_info.resolution[0], bitrate=x.stream_info.average_bandwidth or x.stream_info.bandwidth,
height=x.stream_info.resolution[1], width=x.stream_info.resolution[0],
fps=x.stream_info.frame_rate, height=x.stream_info.resolution[1],
hdr10=(x.stream_info.codecs.split(".")[0] not in ("dvhe", "dvh1") fps=x.stream_info.frame_rate,
and (x.stream_info.video_range or "SDR").strip('"') != "SDR"), hdr10=(x.stream_info.codecs.split(".")[0] not in ("dvhe", "dvh1")
hlg=False, # TODO: Can we get this from the manifest.xml? and (x.stream_info.video_range or "SDR").strip('"') != "SDR"),
dv=x.stream_info.codecs.split(".")[0] in ("dvhe", "dvh1"), hlg=False, # TODO: Can we get this from the manifest.xml?
# switches/options dv=x.stream_info.codecs.split(".")[0] in ("dvhe", "dvh1"),
descriptor=Track.Descriptor.M3U, # switches/options
# decryption descriptor=Track.Descriptor.M3U,
encrypted=bool(master.keys or master.session_keys), # decryption
psshWV=psshWV, encrypted=bool(master.keys or master.session_keys),
psshPR=psshPR, psshWV=psshWV,
# extra psshPR=psshPR,
extra=x # extra
) for x in master.playlists], extra=x
# AUDIO ) for x in master.playlists],
[AudioTrack( # AUDIO
id_=md5(str(x).encode()).hexdigest()[0:6], [AudioTrack(
source=source, id_=md5(str(x).encode()).hexdigest()[0:6],
url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri, source=source,
# metadata original_url=x.base_uri + x.uri,
codec=x.group_id.replace("audio-", "").split("-")[0].split(".")[0], url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri,
language=x.language, # metadata
bitrate=0, # TODO: M3U doesn't seem to state bitrate? codec=x.group_id.replace("audio-", "").split("-")[0].split(".")[0],
channels=x.channels, language=x.language,
atmos=(x.channels or "").endswith("/JOC"), bitrate=0, # TODO: M3U doesn't seem to state bitrate?
descriptive="public.accessibility.describes-video" in (x.characteristics or ""), channels=x.channels,
# switches/options atmos=(x.channels or "").endswith("/JOC"),
descriptor=Track.Descriptor.M3U, descriptive="public.accessibility.describes-video" in (x.characteristics or ""),
# decryption # switches/options
encrypted=False, # don't know for sure if encrypted descriptor=Track.Descriptor.M3U,
psshWV=psshWV, # decryption
psshPR=psshPR, encrypted=False, # don't know for sure if encrypted
# extra psshWV=psshWV,
extra=x psshPR=psshPR,
) for x in master.media if x.type == "AUDIO" and x.uri], # extra
# SUBTITLES extra=x
[TextTrack( ) for x in master.media if x.type == "AUDIO" and x.uri],
id_=md5(str(x).encode()).hexdigest()[0:6], # SUBTITLES
source=source, [TextTrack(
url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri, id_=md5(str(x).encode()).hexdigest()[0:6],
# metadata source=source,
codec="vtt", # assuming VTT, codec info isn't shown original_url=x.base_uri + x.uri,
language=x.language, url=("" if re.match("^https?://", x.uri) else x.base_uri) + x.uri,
forced=x.forced == "YES", # metadata
sdh="public.accessibility.describes-music-and-sound" in (x.characteristics or ""), codec="vtt", # assuming VTT, codec info isn't shown
# switches/options language=x.language,
descriptor=Track.Descriptor.M3U, forced=x.forced == "YES",
# extra sdh="public.accessibility.describes-music-and-sound" in (x.characteristics or ""),
extra=x # switches/options
) for x in master.media if x.type == "SUBTITLES"] 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( tracks.append(VideoTrack(
id_=track_id, id_=track_id,
source=source, source=source,
url=url if source == "HS" else track_url, original_url=url,
url=track_url,
# metadata # metadata
codec=(codecs or "").split(".")[0], codec=(codecs or "").split(".")[0],
language=track_lang, language=track_lang,
@ -324,7 +325,8 @@ def parse(*, url=None, data=None, source, session=None, downloader=None):
tracks.append(AudioTrack( tracks.append(AudioTrack(
id_=track_id, id_=track_id,
source=source, source=source,
url=url if source == "HS" else track_url, original_url=url,
url=track_url,
# metadata # metadata
codec=(codecs or "").split(".")[0], codec=(codecs or "").split(".")[0],
language=track_lang, language=track_lang,
@ -375,6 +377,7 @@ def parse(*, url=None, data=None, source, session=None, downloader=None):
tracks.append(TextTrack( tracks.append(TextTrack(
id_=track_id, id_=track_id,
source=source, source=source,
original_url=url,
url=track_url, url=track_url,
# metadata # metadata
codec=(codecs or "").split(".")[0], codec=(codecs or "").split(".")[0],
@ -390,6 +393,7 @@ def parse(*, url=None, data=None, source, session=None, downloader=None):
tracks.append(TextTrack( tracks.append(TextTrack(
id_=track_id, id_=track_id,
source=source, source=source,
original_url=url,
url=track_url, url=track_url,
# metadata # metadata
codec=(codecs or "").split(".")[0], 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" ffmpeg_binary = shutil.which("ffmpeg") or "/usr/bin/ffmpeg"
arguments = [ arguments = [
executable, executable,
uri, track.original_url or uri,
**["--max-speed", "12M"] if "akamai" in uri else "",
"--save-dir", f'"{os.path.dirname(out)}"', "--save-dir", f'"{os.path.dirname(out)}"',
"--tmp-dir", f'"{os.path.dirname(out)}"', "--tmp-dir", f'"{os.path.dirname(out)}"',
"--save-name", f'"{os.path.basename(out).replace(".mp4", "")}"', "--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": if headers and track.source == "HS":
arguments.extend(["--header", f'"Cookie:{headers["cookie"].replace(" ", "")}"']) arguments.extend(["--header", f'"Cookie:{headers["cookie"].replace(" ", "")}"'])
#for k,v in headers.items(): #for k,v in headers.items():
if "akamai" in uri:
arguments.append("--max-speed")
arguments.append("12M")
if proxy: if proxy:
arguments.extend(["--custom-proxy", proxy]) arguments.extend(["--custom-proxy", proxy])
if not ("linux" in platform): if not ("linux" in platform):
arguments.extend(["--http-request-timeout", "8"]) arguments.extend(["--http-request-timeout", "8"])
if track.__class__.__name__ == "VideoTrack": 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([ arguments.extend([
"-sv", f"res='{track.height}*':codec='{track.codec}':for=best" "-sv", f"res='{track.height}*':codec='{track.codec}':for=best"
]) ])