2025-03-18 00:17:27 +05:30
|
|
|
import base64
|
|
|
|
import re
|
|
|
|
from hashlib import md5
|
|
|
|
|
|
|
|
from vinetrimmer.objects import AudioTrack, TextTrack, Track, Tracks, VideoTrack
|
2025-03-18 00:23:51 +05:30
|
|
|
from vinetrimmer.utils import Cdm
|
2025-03-18 00:17:27 +05:30
|
|
|
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.
|
|
|
|
|
|
|
|
: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`.
|
|
|
|
|
|
|
|
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...")
|
|
|
|
|
|
|
|
# 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"]
|
|
|
|
pssh = keys[0].split(",")[-1] if 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"),
|
2025-03-18 00:23:51 +05:30
|
|
|
hlg=False, # TODO: Can we get this from the manifest?
|
2025-03-18 00:17:27 +05:30
|
|
|
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),
|
|
|
|
pssh=pssh,
|
|
|
|
# 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
|
|
|
|
pssh=pssh,
|
|
|
|
# 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"]
|
|
|
|
)
|