2013-03-02 21:26:28 +01:00
|
|
|
# ex:ts=4:sw=4:sts=4:et
|
|
|
|
# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
|
2019-08-25 00:40:39 +02:00
|
|
|
import binascii
|
|
|
|
import copy
|
2013-02-12 19:39:52 +01:00
|
|
|
import os
|
2018-01-07 20:52:19 +01:00
|
|
|
import time
|
2019-08-25 00:40:39 +02:00
|
|
|
from datetime import datetime
|
|
|
|
from datetime import timedelta
|
2018-07-05 01:24:16 +02:00
|
|
|
|
2019-03-24 21:04:41 +01:00
|
|
|
from cryptography.hazmat.backends import default_backend
|
2019-08-25 00:40:39 +02:00
|
|
|
from cryptography.hazmat.primitives.ciphers import algorithms
|
|
|
|
from cryptography.hazmat.primitives.ciphers import Cipher
|
|
|
|
from cryptography.hazmat.primitives.ciphers import modes
|
|
|
|
from svtplay_dl.error import ServiceError
|
|
|
|
from svtplay_dl.error import UIException
|
2014-04-21 16:50:24 +02:00
|
|
|
from svtplay_dl.fetcher import VideoRetriever
|
2022-12-10 14:05:56 +01:00
|
|
|
from svtplay_dl.fetcher.m3u8 import M3U8
|
|
|
|
from svtplay_dl.subtitle import subtitle_probe
|
2022-06-04 00:45:55 +02:00
|
|
|
from svtplay_dl.utils.fetcher import filter_files
|
2019-08-25 00:40:39 +02:00
|
|
|
from svtplay_dl.utils.http import get_full_url
|
|
|
|
from svtplay_dl.utils.output import ETA
|
2021-05-03 01:43:37 +02:00
|
|
|
from svtplay_dl.utils.output import formatname
|
2019-08-25 00:40:39 +02:00
|
|
|
from svtplay_dl.utils.output import progress_stream
|
|
|
|
from svtplay_dl.utils.output import progressbar
|
2014-04-21 16:50:24 +02:00
|
|
|
|
2014-02-09 15:40:02 +01:00
|
|
|
|
|
|
|
class HLSException(UIException):
|
|
|
|
def __init__(self, url, message):
|
|
|
|
self.url = url
|
2019-08-25 00:33:51 +02:00
|
|
|
super().__init__(message)
|
2014-02-09 15:40:02 +01:00
|
|
|
|
|
|
|
|
|
|
|
class LiveHLSException(HLSException):
|
|
|
|
def __init__(self, url):
|
2019-08-25 00:33:51 +02:00
|
|
|
super().__init__(url, "This is a live HLS stream, and they are not supported.")
|
2014-02-09 15:40:02 +01:00
|
|
|
|
2013-02-12 19:39:52 +01:00
|
|
|
|
2021-05-03 01:43:37 +02:00
|
|
|
def hlsparse(config, res, url, output, **kwargs):
|
2016-10-16 19:35:38 +02:00
|
|
|
if not res:
|
2021-05-16 02:22:37 +02:00
|
|
|
return
|
2016-10-16 19:35:38 +02:00
|
|
|
|
2016-09-09 22:56:05 +02:00
|
|
|
if res.status_code > 400:
|
2021-05-16 02:22:37 +02:00
|
|
|
yield ServiceError(f"Can't read HLS playlist. {res.status_code}")
|
|
|
|
return
|
2018-01-28 20:23:24 +01:00
|
|
|
|
2021-05-22 14:29:54 +02:00
|
|
|
yield from _hlsparse(config, res.text, url, output, cookies=res.cookies, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
def _hlsparse(config, text, url, output, **kwargs):
|
|
|
|
m3u8 = M3U8(text)
|
2018-01-04 00:46:28 +01:00
|
|
|
keycookie = kwargs.pop("keycookie", None)
|
2021-05-22 14:29:54 +02:00
|
|
|
cookies = kwargs.pop("cookies", None)
|
2018-02-12 00:08:09 +01:00
|
|
|
authorization = kwargs.pop("authorization", None)
|
2021-05-03 01:43:37 +02:00
|
|
|
loutput = copy.copy(output)
|
|
|
|
loutput["ext"] = "ts"
|
2020-07-28 21:26:13 +02:00
|
|
|
channels = kwargs.pop("channels", None)
|
|
|
|
codec = kwargs.pop("codec", "h264")
|
2017-10-27 00:08:53 +02:00
|
|
|
media = {}
|
2018-05-27 15:58:11 +02:00
|
|
|
subtitles = {}
|
2021-07-23 14:47:36 +02:00
|
|
|
videos = {}
|
2018-02-12 21:20:48 +01:00
|
|
|
segments = None
|
2018-02-04 22:59:38 +01:00
|
|
|
|
2018-01-14 22:23:58 +01:00
|
|
|
if m3u8.master_playlist:
|
|
|
|
for i in m3u8.master_playlist:
|
|
|
|
audio_url = None
|
2020-07-28 21:26:13 +02:00
|
|
|
vcodec = None
|
|
|
|
chans = None
|
2021-05-22 14:29:54 +02:00
|
|
|
audio_group = None
|
2021-05-16 02:22:37 +02:00
|
|
|
language = ""
|
2021-04-18 14:06:25 +02:00
|
|
|
resolution = ""
|
2018-01-14 22:23:58 +01:00
|
|
|
if i["TAG"] == "EXT-X-MEDIA":
|
2021-05-22 14:29:54 +02:00
|
|
|
if i["TYPE"] and i["TYPE"] != "SUBTITLES":
|
|
|
|
if "URI" in i:
|
|
|
|
if segments is None:
|
|
|
|
segments = True
|
|
|
|
if i["GROUP-ID"] not in media:
|
|
|
|
media[i["GROUP-ID"]] = []
|
|
|
|
if "CHANNELS" in i:
|
|
|
|
if i["CHANNELS"] == "6":
|
|
|
|
chans = "51"
|
|
|
|
if "LANGUAGE" in i:
|
|
|
|
language = i["LANGUAGE"]
|
|
|
|
if "AUTOSELECT" in i and i["AUTOSELECT"].upper() == "YES":
|
|
|
|
role = "main"
|
2018-02-12 21:20:48 +01:00
|
|
|
else:
|
2021-05-22 14:29:54 +02:00
|
|
|
role = "alt"
|
|
|
|
media[i["GROUP-ID"]].append([i["URI"], chans, language, role])
|
|
|
|
else:
|
|
|
|
segments = False
|
2018-05-27 15:58:11 +02:00
|
|
|
if i["TYPE"] == "SUBTITLES":
|
|
|
|
if "URI" in i:
|
2023-10-09 00:09:27 +02:00
|
|
|
caption = None
|
2018-05-27 15:58:11 +02:00
|
|
|
if i["GROUP-ID"] not in subtitles:
|
|
|
|
subtitles[i["GROUP-ID"]] = []
|
2021-06-17 21:54:31 +02:00
|
|
|
if "LANGUAGE" in i:
|
|
|
|
lang = i["LANGUAGE"]
|
|
|
|
else:
|
|
|
|
lang = "und"
|
2023-10-09 00:09:27 +02:00
|
|
|
if "CHARACTERISTICS" in i:
|
|
|
|
caption = True
|
|
|
|
item = [i["URI"], lang, caption]
|
2018-05-27 15:58:11 +02:00
|
|
|
if item not in subtitles[i["GROUP-ID"]]:
|
|
|
|
subtitles[i["GROUP-ID"]].append(item)
|
2018-01-14 22:23:58 +01:00
|
|
|
continue
|
|
|
|
elif i["TAG"] == "EXT-X-STREAM-INF":
|
2019-10-18 15:48:37 +02:00
|
|
|
if "AVERAGE-BANDWIDTH" in i:
|
|
|
|
bit_rate = float(i["AVERAGE-BANDWIDTH"]) / 1000
|
|
|
|
else:
|
|
|
|
bit_rate = float(i["BANDWIDTH"]) / 1000
|
2021-04-18 14:06:25 +02:00
|
|
|
if "RESOLUTION" in i:
|
|
|
|
resolution = i["RESOLUTION"]
|
2020-07-28 21:26:13 +02:00
|
|
|
if "CODECS" in i:
|
|
|
|
if i["CODECS"][:3] == "hvc":
|
|
|
|
vcodec = "hevc"
|
|
|
|
if i["CODECS"][:3] == "avc":
|
|
|
|
vcodec = "h264"
|
2021-07-23 14:47:36 +02:00
|
|
|
if "AUDIO" in i:
|
2021-05-22 14:29:54 +02:00
|
|
|
audio_group = i["AUDIO"]
|
2018-07-05 01:24:16 +02:00
|
|
|
urls = get_full_url(i["URI"], url)
|
2021-07-23 14:47:36 +02:00
|
|
|
videos[bit_rate] = [urls, resolution, vcodec, audio_group]
|
2018-01-14 22:23:58 +01:00
|
|
|
else:
|
2018-01-30 20:11:37 +01:00
|
|
|
continue # Needs to be changed to utilise other tags.
|
2021-05-22 14:29:54 +02:00
|
|
|
|
2021-07-23 14:47:36 +02:00
|
|
|
for bit_rate in list(videos.keys()):
|
|
|
|
urls, resolution, vcodec, audio_group = videos[bit_rate]
|
2021-08-13 11:31:12 +02:00
|
|
|
if audio_group and media:
|
2021-05-22 14:29:54 +02:00
|
|
|
for group in media[audio_group]:
|
|
|
|
audio_url = get_full_url(group[0], url)
|
|
|
|
chans = group[1] if audio_url else channels
|
|
|
|
codec = vcodec if vcodec else codec
|
|
|
|
|
|
|
|
yield HLS(
|
|
|
|
copy.copy(config),
|
|
|
|
urls,
|
|
|
|
bit_rate,
|
|
|
|
cookies=cookies,
|
|
|
|
keycookie=keycookie,
|
|
|
|
authorization=authorization,
|
|
|
|
audio=audio_url,
|
|
|
|
output=loutput,
|
|
|
|
segments=bool(segments),
|
|
|
|
channels=chans,
|
|
|
|
codec=codec,
|
|
|
|
resolution=resolution,
|
|
|
|
language=group[2],
|
|
|
|
role=group[3],
|
|
|
|
**kwargs,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
chans = channels
|
|
|
|
codec = vcodec if vcodec else codec
|
|
|
|
yield HLS(
|
|
|
|
copy.copy(config),
|
|
|
|
urls,
|
|
|
|
bit_rate,
|
|
|
|
cookies=cookies,
|
|
|
|
keycookie=keycookie,
|
|
|
|
authorization=authorization,
|
|
|
|
audio=audio_url,
|
|
|
|
output=loutput,
|
|
|
|
segments=bool(segments),
|
|
|
|
channels=chans,
|
|
|
|
codec=codec,
|
|
|
|
resolution=resolution,
|
|
|
|
**kwargs,
|
|
|
|
)
|
2018-01-14 22:23:58 +01:00
|
|
|
|
2021-05-22 22:54:40 +02:00
|
|
|
if subtitles:
|
2018-05-27 15:58:11 +02:00
|
|
|
for sub in list(subtitles.keys()):
|
|
|
|
for n in subtitles[sub]:
|
2023-10-09 00:09:27 +02:00
|
|
|
subfix = n[2]
|
|
|
|
if len(subtitles[sub]) > 1:
|
|
|
|
if subfix:
|
|
|
|
subfix = f"{n[1]}-caption"
|
2022-12-10 14:05:56 +01:00
|
|
|
yield from subtitle_probe(
|
2023-10-09 00:09:27 +02:00
|
|
|
copy.copy(config), get_full_url(n[0], url), output=copy.copy(output), subfix=subfix, cookies=cookies, **kwargs
|
2019-08-25 00:27:31 +02:00
|
|
|
)
|
2018-05-27 15:58:11 +02:00
|
|
|
|
2018-01-14 22:23:58 +01:00
|
|
|
elif m3u8.media_segment:
|
2018-05-13 13:06:45 +02:00
|
|
|
config.set("segments", False)
|
2021-05-16 02:22:37 +02:00
|
|
|
yield HLS(
|
2020-12-26 13:10:56 +01:00
|
|
|
copy.copy(config),
|
|
|
|
url,
|
|
|
|
0,
|
2021-05-22 14:29:54 +02:00
|
|
|
cookies=cookies,
|
2020-12-26 13:10:56 +01:00
|
|
|
keycookie=keycookie,
|
|
|
|
authorization=authorization,
|
2021-05-03 01:43:37 +02:00
|
|
|
output=loutput,
|
2020-12-26 13:10:56 +01:00
|
|
|
segments=False,
|
2019-08-25 00:27:31 +02:00
|
|
|
)
|
2018-01-14 22:23:58 +01:00
|
|
|
else:
|
2021-05-16 02:22:37 +02:00
|
|
|
yield ServiceError("Can't find HLS playlist in m3u8 file.")
|
2014-04-21 21:42:49 +02:00
|
|
|
|
2015-09-15 20:10:32 +02:00
|
|
|
|
2014-04-21 21:55:39 +02:00
|
|
|
class HLS(VideoRetriever):
|
2018-05-25 22:47:26 +02:00
|
|
|
@property
|
2014-05-01 17:13:46 +02:00
|
|
|
def name(self):
|
|
|
|
return "hls"
|
|
|
|
|
2014-04-21 16:50:24 +02:00
|
|
|
def download(self):
|
2018-05-21 00:05:31 +02:00
|
|
|
self.output_extention = "ts"
|
2018-05-08 22:46:11 +02:00
|
|
|
if self.segments:
|
2020-12-06 18:56:52 +01:00
|
|
|
if self.audio and not self.config.get("only_video"):
|
2021-05-03 01:43:37 +02:00
|
|
|
# self._download(self.audio, file_name=(copy.copy(self.output), "audio.ts"))
|
|
|
|
self._download(self.audio, True)
|
2022-06-09 21:55:53 +02:00
|
|
|
if not self.audio or not self.config.get("only_audio"):
|
2021-05-03 01:43:37 +02:00
|
|
|
self._download(self.url)
|
2017-10-27 00:08:53 +02:00
|
|
|
|
|
|
|
else:
|
2019-08-31 14:30:52 +02:00
|
|
|
# Ignore audio
|
|
|
|
self.audio = None
|
2021-05-03 01:43:37 +02:00
|
|
|
self._download(self.url)
|
2017-10-27 00:08:53 +02:00
|
|
|
|
2021-05-03 01:43:37 +02:00
|
|
|
def _download(self, url, audio=False):
|
2018-03-10 11:40:36 +01:00
|
|
|
cookies = self.kwargs.get("cookies", None)
|
2018-01-07 20:52:19 +01:00
|
|
|
start_time = time.time()
|
2018-02-04 22:59:38 +01:00
|
|
|
m3u8 = M3U8(self.http.request("get", url, cookies=cookies).text)
|
2014-04-21 16:50:24 +02:00
|
|
|
key = None
|
|
|
|
|
2018-03-10 14:09:28 +01:00
|
|
|
def random_iv():
|
2019-03-23 01:00:44 +01:00
|
|
|
return os.urandom(16)
|
2019-08-25 00:27:31 +02:00
|
|
|
|
2021-05-03 01:43:37 +02:00
|
|
|
if audio:
|
|
|
|
self.output["ext"] = "audio.ts"
|
|
|
|
else:
|
|
|
|
self.output["ext"] = "ts"
|
|
|
|
filename = formatname(self.output, self.config)
|
|
|
|
file_d = open(filename, "wb")
|
2014-04-21 16:50:24 +02:00
|
|
|
|
2018-05-08 22:46:11 +02:00
|
|
|
hls_time_stamp = self.kwargs.pop("hls_time_stamp", False)
|
2022-06-03 06:02:34 +02:00
|
|
|
if self.kwargs.get("filter", False):
|
2022-06-04 00:45:55 +02:00
|
|
|
m3u8 = filter_files(m3u8)
|
2017-10-22 22:47:15 +02:00
|
|
|
decryptor = None
|
2018-01-07 20:52:19 +01:00
|
|
|
size_media = len(m3u8.media_segment)
|
|
|
|
eta = ETA(size_media)
|
2018-01-20 12:03:19 +01:00
|
|
|
total_duration = 0
|
2018-01-07 20:52:19 +01:00
|
|
|
duration = 0
|
2018-01-21 11:23:14 +01:00
|
|
|
max_duration = 0
|
2018-01-06 00:08:29 +01:00
|
|
|
for index, i in enumerate(m3u8.media_segment):
|
2022-06-05 16:05:45 +02:00
|
|
|
if "EXTINF" in i and "duration" in i["EXTINF"]:
|
2018-01-20 12:03:19 +01:00
|
|
|
duration = i["EXTINF"]["duration"]
|
2018-01-21 11:23:14 +01:00
|
|
|
max_duration = max(max_duration, duration)
|
2018-01-20 12:03:19 +01:00
|
|
|
total_duration += duration
|
2018-07-05 01:24:16 +02:00
|
|
|
item = get_full_url(i["URI"], url)
|
2014-04-21 16:50:24 +02:00
|
|
|
|
2018-05-08 22:46:11 +02:00
|
|
|
if not self.config.get("silent"):
|
|
|
|
if self.config.get("live"):
|
2019-09-06 22:49:49 +02:00
|
|
|
progressbar(size_media, index + 1, "".join(["DU: ", str(timedelta(seconds=int(total_duration)))]))
|
2018-01-07 20:52:19 +01:00
|
|
|
else:
|
|
|
|
eta.increment()
|
2019-08-25 00:27:31 +02:00
|
|
|
progressbar(size_media, index + 1, "".join(["ETA: ", str(eta)]))
|
2014-04-21 16:50:24 +02:00
|
|
|
|
2022-06-05 16:05:45 +02:00
|
|
|
headers = {}
|
|
|
|
if "EXT-X-BYTERANGE" in i:
|
|
|
|
headers["Range"] = f'bytes={i["EXT-X-BYTERANGE"]["o"]}-{i["EXT-X-BYTERANGE"]["o"] + i["EXT-X-BYTERANGE"]["n"] - 1}'
|
|
|
|
data = self.http.request("get", item, cookies=cookies, headers=headers)
|
2015-08-30 00:06:20 +02:00
|
|
|
if data.status_code == 404:
|
|
|
|
break
|
|
|
|
data = data.content
|
2021-03-10 14:11:49 +01:00
|
|
|
|
2017-10-21 21:38:03 +02:00
|
|
|
if m3u8.encrypted:
|
2018-02-12 00:08:09 +01:00
|
|
|
headers = {}
|
2017-10-21 21:38:03 +02:00
|
|
|
if self.keycookie:
|
|
|
|
keycookies = self.keycookie
|
|
|
|
else:
|
|
|
|
keycookies = cookies
|
2018-02-12 00:08:09 +01:00
|
|
|
if self.authorization:
|
|
|
|
headers["authorization"] = self.authorization
|
2017-10-21 21:38:03 +02:00
|
|
|
|
2017-10-22 22:47:15 +02:00
|
|
|
# Update key/decryptor
|
2018-01-06 00:08:29 +01:00
|
|
|
if "EXT-X-KEY" in i:
|
2018-07-05 01:24:16 +02:00
|
|
|
keyurl = get_full_url(i["EXT-X-KEY"]["URI"], url)
|
2018-10-13 13:53:10 +02:00
|
|
|
if keyurl and keyurl[:4] == "skd:":
|
|
|
|
raise HLSException(keyurl, "Can't decrypt beacuse of DRM")
|
2019-09-06 22:49:49 +02:00
|
|
|
key = self.http.request("get", keyurl, cookies=keycookies, headers=headers).content
|
|
|
|
iv = binascii.unhexlify(i["EXT-X-KEY"]["IV"][2:].zfill(32)) if "IV" in i["EXT-X-KEY"] else random_iv()
|
2019-03-23 01:00:44 +01:00
|
|
|
backend = default_backend()
|
|
|
|
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
|
|
|
|
decryptor = cipher.decryptor()
|
2018-01-06 00:08:29 +01:00
|
|
|
|
2020-12-31 15:18:56 +01:00
|
|
|
# In some cases the playlist say its encrypted but the files is not.
|
|
|
|
# This happen on svtplay 5.1ch stream where it started with ID3..
|
|
|
|
# Adding the other ones is header for mpeg-ts files. third byte is 10 or 11..
|
2021-03-10 14:11:49 +01:00
|
|
|
if data[:3] != b"ID3" and data[:3] != b"\x47\x40\x11" and data[:3] != b"\x47\x40\x10" and data[4:12] != b"ftypisom":
|
2020-12-31 15:18:56 +01:00
|
|
|
if decryptor:
|
|
|
|
data = decryptor.update(data)
|
|
|
|
else:
|
|
|
|
raise ValueError("No decryptor found for encrypted hls steam.")
|
2014-04-21 16:50:24 +02:00
|
|
|
file_d.write(data)
|
2013-02-12 19:39:52 +01:00
|
|
|
|
2019-09-06 22:49:49 +02:00
|
|
|
if self.config.get("capture_time") > 0 and total_duration >= self.config.get("capture_time") * 60:
|
2018-01-07 21:49:50 +01:00
|
|
|
break
|
|
|
|
|
2018-05-08 22:46:11 +02:00
|
|
|
if (size_media == (index + 1)) and self.config.get("live"):
|
2018-01-21 11:23:14 +01:00
|
|
|
sleep_int = (start_time + max_duration * 2) - time.time()
|
|
|
|
if sleep_int > 0:
|
|
|
|
time.sleep(sleep_int)
|
2018-01-07 20:52:19 +01:00
|
|
|
|
2018-01-21 11:23:14 +01:00
|
|
|
size_media_old = size_media
|
|
|
|
while size_media_old == size_media:
|
|
|
|
start_time = time.time()
|
2018-01-15 22:16:07 +01:00
|
|
|
|
2018-05-08 22:46:11 +02:00
|
|
|
if hls_time_stamp:
|
2019-09-06 22:49:49 +02:00
|
|
|
end_time_stamp = (datetime.utcnow() - timedelta(minutes=1, seconds=max_duration * 2)).replace(microsecond=0)
|
2018-01-21 11:23:14 +01:00
|
|
|
start_time_stamp = end_time_stamp - timedelta(minutes=1)
|
2018-01-15 22:16:07 +01:00
|
|
|
|
2018-02-04 22:59:38 +01:00
|
|
|
base_url = url.split(".m3u8")[0]
|
2021-02-28 22:05:15 +01:00
|
|
|
url = f"{base_url}.m3u8?in={start_time_stamp.isoformat()}&out={end_time_stamp.isoformat()}?"
|
2018-01-15 22:16:07 +01:00
|
|
|
|
2018-02-04 22:59:38 +01:00
|
|
|
new_m3u8 = M3U8(self.http.request("get", url, cookies=cookies).text)
|
2018-01-21 11:23:14 +01:00
|
|
|
for n_m3u in new_m3u8.media_segment:
|
2019-09-06 22:49:49 +02:00
|
|
|
if not any(d["URI"] == n_m3u["URI"] for d in m3u8.media_segment):
|
2018-01-21 11:23:14 +01:00
|
|
|
m3u8.media_segment.append(n_m3u)
|
2018-01-07 20:52:19 +01:00
|
|
|
|
2018-01-21 11:23:14 +01:00
|
|
|
size_media = len(m3u8.media_segment)
|
|
|
|
|
|
|
|
if size_media_old == size_media:
|
|
|
|
time.sleep(max_duration)
|
2018-01-07 20:52:19 +01:00
|
|
|
|
2017-11-24 23:11:48 +01:00
|
|
|
file_d.close()
|
2018-05-08 22:46:11 +02:00
|
|
|
if not self.config.get("silent"):
|
2019-08-25 00:27:31 +02:00
|
|
|
progress_stream.write("\n")
|
2017-11-24 23:11:48 +01:00
|
|
|
self.finished = True
|