From a8edede94b375987bb2a3d48a0931cc94d2205fc Mon Sep 17 00:00:00 2001 From: chu23465 <130033130+chu23465@users.noreply.github.com> Date: Wed, 16 Apr 2025 17:20:32 +0530 Subject: [PATCH] Fix for M3U8 Hotstar --- vinetrimmer/objects/tracks.py | 4 +- vinetrimmer/parsers/ism.py | 1 + vinetrimmer/parsers/m3u8.py | 213 +++++++++++++++++----------------- vinetrimmer/parsers/mpd.py | 8 +- vinetrimmer/utils/io.py | 11 +- 5 files changed, 125 insertions(+), 112 deletions(-) diff --git a/vinetrimmer/objects/tracks.py b/vinetrimmer/objects/tracks.py index e2ceec2..c43c49c 100644 --- a/vinetrimmer/objects/tracks.py +++ b/vinetrimmer/objects/tracks.py @@ -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 diff --git a/vinetrimmer/parsers/ism.py b/vinetrimmer/parsers/ism.py index 9d09c53..f0adf70 100644 --- a/vinetrimmer/parsers/ism.py +++ b/vinetrimmer/parsers/ism.py @@ -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], diff --git a/vinetrimmer/parsers/m3u8.py b/vinetrimmer/parsers/m3u8.py index 1cbe7f0..063a34d 100644 --- a/vinetrimmer/parsers/m3u8.py +++ b/vinetrimmer/parsers/m3u8.py @@ -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"] + ) diff --git a/vinetrimmer/parsers/mpd.py b/vinetrimmer/parsers/mpd.py index 819748a..511365b 100644 --- a/vinetrimmer/parsers/mpd.py +++ b/vinetrimmer/parsers/mpd.py @@ -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], diff --git a/vinetrimmer/utils/io.py b/vinetrimmer/utils/io.py index 7640a96..50f538c 100644 --- a/vinetrimmer/utils/io.py +++ b/vinetrimmer/utils/io.py @@ -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" ])