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 -*-
|
2013-03-01 23:39:42 +01:00
|
|
|
from __future__ import absolute_import
|
2013-01-17 00:21:47 +01:00
|
|
|
import base64
|
|
|
|
import struct
|
|
|
|
import logging
|
|
|
|
import binascii
|
2015-10-04 14:35:35 +02:00
|
|
|
import copy
|
2013-01-17 00:21:47 +01:00
|
|
|
import xml.etree.ElementTree as ET
|
2018-01-30 22:07:21 +01:00
|
|
|
from urllib.parse import urlparse
|
2013-01-17 00:21:47 +01:00
|
|
|
|
2018-05-13 13:06:45 +02:00
|
|
|
from svtplay_dl.utils.output import progressbar, progress_stream, ETA, output
|
2014-02-11 18:27:21 +01:00
|
|
|
from svtplay_dl.error import UIException
|
2014-04-21 16:50:24 +02:00
|
|
|
from svtplay_dl.fetcher import VideoRetriever
|
2015-10-04 17:40:40 +02:00
|
|
|
from svtplay_dl.error import ServiceError
|
2013-01-17 00:21:47 +01:00
|
|
|
|
2018-05-13 13:06:45 +02:00
|
|
|
|
2013-01-17 00:21:47 +01:00
|
|
|
log = logging.getLogger('svtplay_dl')
|
|
|
|
|
2014-01-07 20:52:03 +01: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
|
|
|
|
2014-02-11 18:27:21 +01:00
|
|
|
class HDSException(UIException):
|
|
|
|
def __init__(self, url, message):
|
|
|
|
self.url = url
|
|
|
|
super(HDSException, self).__init__(message)
|
|
|
|
|
|
|
|
|
|
|
|
class LiveHDSException(HDSException):
|
|
|
|
def __init__(self, url):
|
|
|
|
super(LiveHDSException, self).__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 = {}
|
2016-10-16 19:35:38 +02:00
|
|
|
|
|
|
|
if not res:
|
2018-05-08 22:48:55 +02:00
|
|
|
return streams
|
2016-10-16 19:35:38 +02:00
|
|
|
|
2016-09-09 22:56:05 +02:00
|
|
|
if res.status_code >= 400:
|
|
|
|
streams[0] = ServiceError("Can't read HDS playlist. {0}".format(res.status_code))
|
2015-10-04 17:40:40 +02:00
|
|
|
return streams
|
2016-08-17 00:43:24 +02:00
|
|
|
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
|
|
|
|
2014-10-12 23:31:02 +02:00
|
|
|
if xml.find("{http://ns.adobe.com/f4m/1.0}drmAdditionalHeader") is not None:
|
2015-10-04 17:39:43 +02:00
|
|
|
streams[0] = ServiceError("HDS DRM protected content.")
|
|
|
|
return streams
|
2014-04-27 13:19:34 +02:00
|
|
|
for i in bootstrapIter:
|
2014-10-12 23:18:33 +02:00
|
|
|
if "id" in i.attrib:
|
|
|
|
bootstrap[i.attrib["id"]] = i.text
|
|
|
|
else:
|
|
|
|
bootstrap["0"] = i.text
|
2014-07-10 18:07:59 +02:00
|
|
|
parse = urlparse(manifest)
|
|
|
|
querystring = parse.query
|
2018-01-21 12:07:27 +01:00
|
|
|
url = "{0}://{1}{2}".format(parse.scheme, parse.netloc, parse.path)
|
2014-04-27 13:19:34 +02:00
|
|
|
for i in mediaIter:
|
2016-01-17 00:36:15 +01:00
|
|
|
bootstrapid = bootstrap[i.attrib["bootstrapInfoId"]]
|
2018-05-08 22:46:11 +02:00
|
|
|
streams[int(i.attrib["bitrate"])] = HDS(copy.copy(config), url, i.attrib["bitrate"], url_id=i.attrib["url"],
|
2018-01-30 20:11:37 +01:00
|
|
|
bootstrap=bootstrapid,
|
|
|
|
metadata=i.find("{http://ns.adobe.com/f4m/1.0}metadata").text,
|
2018-05-13 13:06:45 +02:00
|
|
|
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):
|
2014-05-01 17:13:46 +02:00
|
|
|
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)
|
|
|
|
|
2014-07-10 18:07:59 +02:00
|
|
|
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])
|
2018-01-21 12:07:27 +01: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")
|
2017-11-24 23:11:48 +01:00
|
|
|
if file_d is None:
|
2014-08-20 20:27:45 +02:00
|
|
|
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"))
|
2014-07-10 18:07:59 +02:00
|
|
|
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:
|
2018-01-21 12:07:27 +01:00
|
|
|
url = "{0}/{1}Seg1-Frag{2}?{3}".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)
|
|
|
|
progressbar(total, i, ''.join(["ETA: ", str(eta)]))
|
2015-10-04 14:36:21 +02:00
|
|
|
data = self.http.request("get", url, cookies=cookies)
|
2015-08-30 00:06:20 +02:00
|
|
|
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
|
2014-07-10 18:07:59 +02:00
|
|
|
start += 1
|
2014-04-21 16:50:24 +02: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"):
|
2017-11-24 23:11:48 +01:00
|
|
|
progress_stream.write('\n')
|
|
|
|
self.finished = True
|
2016-03-22 22:28:41 +01:00
|
|
|
|
2013-01-17 00:21:47 +01:00
|
|
|
|
|
|
|
def readbyte(data, pos):
|
2014-01-07 20:52:03 +01:00
|
|
|
return struct.unpack("B", bytes(_chr(data[pos]), "ascii"))[0]
|
2013-01-17 00:21:47 +01:00
|
|
|
|
2015-09-15 20:10:32 +02:00
|
|
|
|
2013-01-17 00:21:47 +01:00
|
|
|
def read16(data, pos):
|
|
|
|
endpos = pos + 2
|
|
|
|
return struct.unpack(">H", data[pos:endpos])[0]
|
|
|
|
|
2015-09-15 20:10:32 +02:00
|
|
|
|
2013-01-17 00:21:47 +01: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
|
|
|
|
2013-01-17 00:21:47 +01:00
|
|
|
def read32(data, pos):
|
|
|
|
end = pos + 4
|
|
|
|
return struct.unpack(">i", data[pos:end])[0]
|
|
|
|
|
2015-09-15 20:10:32 +02:00
|
|
|
|
2014-07-10 18:07:59 +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
|
|
|
|
2013-01-17 00:21:47 +01:00
|
|
|
def read64(data, pos):
|
|
|
|
end = pos + 8
|
|
|
|
return struct.unpack(">Q", data[pos:end])[0]
|
|
|
|
|
2015-09-15 20:10:32 +02:00
|
|
|
|
2013-01-17 00:21:47 +01:00
|
|
|
def readstring(data, pos):
|
|
|
|
length = 0
|
2014-01-07 20:52:03 +01:00
|
|
|
while bytes(_chr(data[pos + length]), "ascii") != b"\x00":
|
2013-01-17 00:21:47 +01:00
|
|
|
length += 1
|
|
|
|
endpos = pos + length
|
|
|
|
string = data[pos:endpos]
|
|
|
|
pos += length + 1
|
|
|
|
return pos, string
|
|
|
|
|
2015-09-15 20:10:32 +02:00
|
|
|
|
2013-01-17 00:21:47 +01: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.
|
2013-01-17 00:21:47 +01:00
|
|
|
def readbox(data, pos):
|
2014-07-28 16:01:27 +02:00
|
|
|
# version = readbyte(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += 1
|
2014-07-28 16:01:27 +02:00
|
|
|
# flags = read24(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += 3
|
2014-07-28 16:01:27 +02:00
|
|
|
# bootstrapversion = read32(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += 4
|
2014-07-28 16:01:27 +02:00
|
|
|
# byte = readbyte(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
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)
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += 4
|
2014-07-28 16:01:27 +02:00
|
|
|
# currentmediatime = read64(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += 8
|
2014-07-28 16:01:27 +02:00
|
|
|
# smptetimecodeoffset = read64(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += 8
|
|
|
|
temp = readstring(data, pos)
|
2014-07-28 16:01:27 +02:00
|
|
|
# movieidentifier = temp[1]
|
2013-01-17 00:21:47 +01:00
|
|
|
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]
|
2013-01-17 00:21:47 +01:00
|
|
|
pos = tmp[0]
|
|
|
|
tmp = readstring(data, pos)
|
2014-07-28 16:01:27 +02:00
|
|
|
# metadata = tmp[1]
|
2013-01-17 00:21:47 +01:00
|
|
|
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":
|
2013-01-17 00:21:47 +01:00
|
|
|
antal = readasrtbox(data, pos)
|
|
|
|
pos += boxsize
|
|
|
|
fragRunTableCount = readbyte(data, pos)
|
|
|
|
pos += 1
|
|
|
|
i = 0
|
2014-07-10 18:07:59 +02:00
|
|
|
first = 1
|
2013-01-17 00:21:47 +01:00
|
|
|
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":
|
2014-07-10 18:07:59 +02:00
|
|
|
first = readafrtbox(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += boxsize
|
|
|
|
i += 1
|
2014-07-10 18:07:59 +02:00
|
|
|
antal[1]["first"] = first
|
2013-01-17 00:21:47 +01:00
|
|
|
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.
|
2013-01-17 00:21:47 +01:00
|
|
|
def readafrtbox(data, pos):
|
2014-07-28 16:01:27 +02:00
|
|
|
# version = readbyte(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += 1
|
2014-07-28 16:01:27 +02:00
|
|
|
# flags = read24(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += 3
|
2014-07-28 16:01:27 +02:00
|
|
|
# timescale = read32(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
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]
|
2013-01-17 00:21:47 +01:00
|
|
|
pos = temp[0]
|
|
|
|
i += 1
|
|
|
|
fragrunentrycount = read32(data, pos)
|
|
|
|
pos += 4
|
|
|
|
i = 0
|
2014-07-10 18:07:59 +02:00
|
|
|
first = 1
|
|
|
|
skip = False
|
2013-01-17 00:21:47 +01:00
|
|
|
while i < fragrunentrycount:
|
2014-07-10 18:07:59 +02:00
|
|
|
firstfragment = readu32(data, pos)
|
|
|
|
if not skip:
|
|
|
|
first = firstfragment
|
|
|
|
skip = True
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += 4
|
2014-07-28 16:01:27 +02:00
|
|
|
# timestamp = read64(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += 8
|
2014-07-28 16:01:27 +02:00
|
|
|
# duration = read32(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += 4
|
|
|
|
i += 1
|
2014-07-10 18:07:59 +02:00
|
|
|
return first
|
2013-01-17 00:21:47 +01:00
|
|
|
|
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.
|
2013-01-17 00:21:47 +01:00
|
|
|
def readasrtbox(data, pos):
|
2014-07-28 16:01:27 +02:00
|
|
|
# version = readbyte(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
pos += 1
|
2014-07-28 16:01:27 +02:00
|
|
|
# flags = read24(data, pos)
|
2013-01-17 00:21:47 +01:00
|
|
|
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
|
|
|
|
2013-01-17 00:21:47 +01: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:
|
2013-03-10 13:41:56 +01:00
|
|
|
tagLen, = struct.unpack_from(">L", fragData, start)
|
|
|
|
tagLen &= 0x00ffffff
|
2014-12-26 02:04:29 +01:00
|
|
|
start += tagLen + 11 + 4
|
2013-01-17 00:21:47 +01:00
|
|
|
return start
|