Fix for M3U8 Hotstar
This commit is contained in:
parent
5b88624051
commit
a8edede94b
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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],
|
||||||
|
|||||||
@ -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"]
|
||||||
|
)
|
||||||
|
|||||||
@ -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],
|
||||||
|
|||||||
@ -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"
|
||||||
])
|
])
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user