1
0
mirror of https://github.com/spaam/svtplay-dl.git synced 2024-11-24 12:15:40 +01:00
svtplay-dl/lib/svtplay_dl/fetcher/hds.py

332 lines
8.8 KiB
Python
Raw Normal View History

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 -*-
import base64
import binascii
import copy
2019-08-25 00:40:39 +02:00
import struct
import xml.etree.ElementTree as ET
2018-01-30 22:07:21 +01:00
from urllib.parse import urlparse
2019-08-25 00:40:39 +02:00
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
2019-08-25 00:40:39 +02:00
from svtplay_dl.utils.output import ETA
from svtplay_dl.utils.output import output
from svtplay_dl.utils.output import progress_stream
from svtplay_dl.utils.output import progressbar
2018-05-13 13:06:45 +02:00
2018-01-13 20:27:40 +01:00
def _chr(temp):
return chr(temp)
2014-01-07 20:52:03 +01:00
2018-01-30 20:11:37 +01:00
class HDSException(UIException):
def __init__(self, url, message):
self.url = url
2019-08-25 00:33:51 +02:00
super().__init__(message)
class LiveHDSException(HDSException):
def __init__(self, url):
2019-08-25 00:33:51 +02:00
super().__init__(url, "This is a live HDS stream, and they are not supported.")
2015-09-15 20:10:32 +02:00
2018-05-08 22:46:11 +02:00
def hdsparse(config, res, manifest, output=None):
2014-04-27 13:19:34 +02:00
streams = {}
bootstrap = {}
if not res:
2018-05-08 22:48:55 +02:00
return streams
if res.status_code >= 400:
2021-02-28 22:05:15 +01:00
streams[0] = ServiceError(f"Can't read HDS playlist. {res.status_code}")
return streams
data = res.text
xml = ET.XML(data)
2014-04-27 13:19:34 +02:00
2018-01-13 20:27:40 +01:00
bootstrapIter = xml.iter("{http://ns.adobe.com/f4m/1.0}bootstrapInfo")
mediaIter = xml.iter("{http://ns.adobe.com/f4m/1.0}media")
2014-04-27 13:19:34 +02:00
if xml.find("{http://ns.adobe.com/f4m/1.0}drmAdditionalHeader") is not None:
streams[0] = ServiceError("HDS DRM protected content.")
return streams
2014-04-27 13:19:34 +02:00
for i in bootstrapIter:
if "id" in i.attrib:
bootstrap[i.attrib["id"]] = i.text
else:
bootstrap["0"] = i.text
parse = urlparse(manifest)
querystring = parse.query
2021-02-28 22:05:15 +01:00
url = f"{parse.scheme}://{parse.netloc}{parse.path}"
2014-04-27 13:19:34 +02:00
for i in mediaIter:
bootstrapid = bootstrap[i.attrib["bootstrapInfoId"]]
2019-08-25 00:27:31 +02:00
streams[int(i.attrib["bitrate"])] = HDS(
copy.copy(config),
url,
i.attrib["bitrate"],
url_id=i.attrib["url"],
bootstrap=bootstrapid,
metadata=i.find("{http://ns.adobe.com/f4m/1.0}metadata").text,
querystring=querystring,
cookies=res.cookies,
output=output,
)
2014-04-27 13:19:34 +02:00
return streams
2015-09-15 20:10:32 +02:00
2014-04-21 16:50:24 +02:00
class HDS(VideoRetriever):
2018-05-25 22:47:26 +02:00
@property
def name(self):
return "hds"
2014-04-21 16:50:24 +02:00
def download(self):
2018-05-13 01:45:23 +02:00
self.output_extention = "flv"
2018-05-08 22:46:11 +02:00
if self.config.get("live") and not self.config.get("force"):
2014-04-21 16:50:24 +02:00
raise LiveHDSException(self.url)
querystring = self.kwargs["querystring"]
2015-10-04 14:36:21 +02:00
cookies = self.kwargs["cookies"]
2014-04-27 13:19:34 +02:00
bootstrap = base64.b64decode(self.kwargs["bootstrap"])
2014-04-21 16:50:24 +02:00
box = readboxtype(bootstrap, 0)
antal = None
if box[2] == b"abst":
antal = readbox(bootstrap, box[0])
2019-08-25 00:27:31 +02:00
baseurl = self.url[0 : self.url.rfind("/")]
2014-04-21 16:50:24 +02:00
2018-05-08 22:46:11 +02:00
file_d = output(self.output, self.config, "flv")
if file_d is None:
return
2014-04-21 16:50:24 +02:00
2014-04-27 13:19:34 +02:00
metasize = struct.pack(">L", len(base64.b64decode(self.kwargs["metadata"])))[1:]
2014-04-21 16:50:24 +02:00
file_d.write(binascii.a2b_hex(b"464c560105000000090000000012"))
file_d.write(metasize)
file_d.write(binascii.a2b_hex(b"00000000000000"))
2014-04-27 13:19:34 +02:00
file_d.write(base64.b64decode(self.kwargs["metadata"]))
2014-04-21 16:50:24 +02:00
file_d.write(binascii.a2b_hex(b"00000000"))
i = 1
start = antal[1]["first"]
2014-04-21 16:50:24 +02:00
total = antal[1]["total"]
2014-04-27 13:22:02 +02:00
eta = ETA(total)
2014-04-21 16:50:24 +02:00
while i <= total:
url = "{}/{}Seg1-Frag{}?{}".format(baseurl, self.kwargs["url_id"], start, querystring)
2018-05-08 22:46:11 +02:00
if not self.config.get("silent"):
2014-04-21 16:50:24 +02:00
eta.update(i)
2019-08-25 00:27:31 +02:00
progressbar(total, i, "".join(["ETA: ", str(eta)]))
2015-10-04 14:36:21 +02:00
data = self.http.request("get", url, cookies=cookies)
if data.status_code == 404:
break
data = data.content
2014-04-21 16:50:24 +02:00
number = decode_f4f(i, data)
file_d.write(data[number:])
i += 1
start += 1
2014-04-21 16:50:24 +02: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")
self.finished = True
def readbyte(data, pos):
2014-01-07 20:52:03 +01:00
return struct.unpack("B", bytes(_chr(data[pos]), "ascii"))[0]
2015-09-15 20:10:32 +02:00
def read16(data, pos):
endpos = pos + 2
return struct.unpack(">H", data[pos:endpos])[0]
2015-09-15 20:10:32 +02:00
def read24(data, pos):
end = pos + 3
return struct.unpack(">L", "\x00" + data[pos:end])[0]
2015-09-15 20:10:32 +02:00
def read32(data, pos):
end = pos + 4
return struct.unpack(">i", data[pos:end])[0]
2015-09-15 20:10:32 +02:00
def readu32(data, pos):
end = pos + 4
return struct.unpack(">I", data[pos:end])[0]
2015-09-15 20:10:32 +02:00
def read64(data, pos):
end = pos + 8
return struct.unpack(">Q", data[pos:end])[0]
2015-09-15 20:10:32 +02:00
def readstring(data, pos):
length = 0
2014-01-07 20:52:03 +01:00
while bytes(_chr(data[pos + length]), "ascii") != b"\x00":
length += 1
endpos = pos + length
string = data[pos:endpos]
pos += length + 1
return pos, string
2015-09-15 20:10:32 +02:00
def readboxtype(data, pos):
boxsize = read32(data, pos)
tpos = pos + 4
endpos = tpos + 4
boxtype = data[tpos:endpos]
if boxsize > 1:
boxsize -= 8
pos += 8
return pos, boxsize, boxtype
2015-09-15 20:10:32 +02:00
2013-04-21 12:33:35 +02:00
# Note! A lot of variable assignments are commented out. These are
# accessible values that we currently don't use.
def readbox(data, pos):
2014-07-28 16:01:27 +02:00
# version = readbyte(data, pos)
pos += 1
2014-07-28 16:01:27 +02:00
# flags = read24(data, pos)
pos += 3
2014-07-28 16:01:27 +02:00
# bootstrapversion = read32(data, pos)
pos += 4
2014-07-28 16:01:27 +02:00
# byte = readbyte(data, pos)
pos += 1
2014-07-28 16:01:27 +02:00
# profile = (byte & 0xC0) >> 6
# live = (byte & 0x20) >> 5
# update = (byte & 0x10) >> 4
# timescale = read32(data, pos)
pos += 4
2014-07-28 16:01:27 +02:00
# currentmediatime = read64(data, pos)
pos += 8
2014-07-28 16:01:27 +02:00
# smptetimecodeoffset = read64(data, pos)
pos += 8
temp = readstring(data, pos)
2014-07-28 16:01:27 +02:00
# movieidentifier = temp[1]
pos = temp[0]
serverentrycount = readbyte(data, pos)
pos += 1
serverentrytable = []
i = 0
while i < serverentrycount:
temp = readstring(data, pos)
serverentrytable.append(temp[1])
pos = temp[0]
i += 1
qualityentrycount = readbyte(data, pos)
pos += 1
qualityentrytable = []
i = 0
while i < qualityentrycount:
temp = readstring(data, pos)
qualityentrytable.append(temp[1])
pos = temp[0]
i += 1
tmp = readstring(data, pos)
2014-07-28 16:01:27 +02:00
# drm = tmp[1]
pos = tmp[0]
tmp = readstring(data, pos)
2014-07-28 16:01:27 +02:00
# metadata = tmp[1]
pos = tmp[0]
segmentruntable = readbyte(data, pos)
pos += 1
if segmentruntable > 0:
tmp = readboxtype(data, pos)
boxtype = tmp[2]
boxsize = tmp[1]
pos = tmp[0]
2014-01-07 20:52:03 +01:00
if boxtype == b"asrt":
antal = readasrtbox(data, pos)
pos += boxsize
fragRunTableCount = readbyte(data, pos)
pos += 1
i = 0
first = 1
while i < fragRunTableCount:
tmp = readboxtype(data, pos)
boxtype = tmp[2]
boxsize = tmp[1]
pos = tmp[0]
2014-01-07 20:52:03 +01:00
if boxtype == b"afrt":
first = readafrtbox(data, pos)
pos += boxsize
i += 1
antal[1]["first"] = first
return antal
2015-09-15 20:10:32 +02:00
2013-04-21 12:33:35 +02:00
# Note! A lot of variable assignments are commented out. These are
# accessible values that we currently don't use.
def readafrtbox(data, pos):
2014-07-28 16:01:27 +02:00
# version = readbyte(data, pos)
pos += 1
2014-07-28 16:01:27 +02:00
# flags = read24(data, pos)
pos += 3
2014-07-28 16:01:27 +02:00
# timescale = read32(data, pos)
pos += 4
qualityentry = readbyte(data, pos)
pos += 1
i = 0
while i < qualityentry:
temp = readstring(data, pos)
2014-08-12 00:08:51 +02:00
# qualitysegmulti = temp[1]
pos = temp[0]
i += 1
fragrunentrycount = read32(data, pos)
pos += 4
i = 0
first = 1
skip = False
while i < fragrunentrycount:
firstfragment = readu32(data, pos)
if not skip:
first = firstfragment
skip = True
pos += 4
2014-07-28 16:01:27 +02:00
# timestamp = read64(data, pos)
pos += 8
2014-07-28 16:01:27 +02:00
# duration = read32(data, pos)
pos += 4
i += 1
return first
2015-09-15 20:10:32 +02:00
2013-04-21 12:33:35 +02:00
# Note! A lot of variable assignments are commented out. These are
# accessible values that we currently don't use.
def readasrtbox(data, pos):
2014-07-28 16:01:27 +02:00
# version = readbyte(data, pos)
pos += 1
2014-07-28 16:01:27 +02:00
# flags = read24(data, pos)
pos += 3
qualityentrycount = readbyte(data, pos)
pos += 1
qualitysegmentmodifers = []
i = 0
while i < qualityentrycount:
temp = readstring(data, pos)
qualitysegmentmodifers.append(temp[1])
pos = temp[0]
i += 1
seqCount = read32(data, pos)
pos += 4
ret = {}
i = 0
while i < seqCount:
firstseg = read32(data, pos)
pos += 4
fragPerSeg = read32(data, pos)
pos += 4
tmp = i + 1
ret[tmp] = {"first": firstseg, "total": fragPerSeg}
i += 1
return ret
2015-09-15 20:10:32 +02:00
def decode_f4f(fragID, fragData):
2014-01-07 20:52:03 +01:00
start = fragData.find(b"mdat") + 4
2014-12-26 02:04:29 +01:00
if fragID > 1:
2020-05-03 11:38:40 +02:00
(tagLen,) = struct.unpack_from(">L", fragData, start)
2019-08-25 00:27:31 +02:00
tagLen &= 0x00FFFFFF
2014-12-26 02:04:29 +01:00
start += tagLen + 11 + 4
return start