streamlink/src/streamlink/plugins/abematv.py

269 lines
9.5 KiB
Python

"""
$description Japanese live TV streaming website with multiple channels including news, sports, entertainment and anime.
$url abema.tv
$type live, vod
$region Japan
"""
import hashlib
import hmac
import logging
import re
import struct
import time
import uuid
from base64 import urlsafe_b64encode
from binascii import unhexlify
from Crypto.Cipher import AES
from requests import Response
from requests.adapters import BaseAdapter
from streamlink.exceptions import NoStreamsError
from streamlink.plugin import Plugin, pluginmatcher
from streamlink.plugin.api import useragents, validate
from streamlink.stream.hls import HLSStream, HLSStreamReader, HLSStreamWriter
from streamlink.utils.url import update_qsd
log = logging.getLogger(__name__)
class AbemaTVHLSStreamWriter(HLSStreamWriter):
def should_filter_sequence(self, sequence):
return "/tsad/" in sequence.segment.uri or super().should_filter_sequence(sequence)
class AbemaTVHLSStreamReader(HLSStreamReader):
__writer__ = AbemaTVHLSStreamWriter
class AbemaTVHLSStream(HLSStream):
__reader__ = AbemaTVHLSStreamReader
class AbemaTVLicenseAdapter(BaseAdapter):
"""
Handling abematv-license:// protocol to get real video key_data.
"""
STRTABLE = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
HKEY = b"3AF0298C219469522A313570E8583005A642E73EDD58E3EA2FB7339D3DF1597E"
_MEDIATOKEN_API = "https://api.abema.io/v1/media/token"
_LICENSE_API = "https://license.abema.io/abematv-hls"
_MEDIATOKEN_SCHEMA = validate.Schema({"token": str})
_LICENSE_SCHEMA = validate.Schema({"k": str, "cid": str})
def __init__(self, session, deviceid, usertoken):
self._session = session
self.deviceid = deviceid
self.usertoken = usertoken
super().__init__()
def _get_videokey_from_ticket(self, ticket):
params = {
"osName": "android",
"osVersion": "6.0.1",
"osLang": "ja_JP",
"osTimezone": "Asia/Tokyo",
"appId": "tv.abema",
"appVersion": "3.27.1",
}
auth_header = {"Authorization": f"Bearer {self.usertoken}"}
res = self._session.http.get(self._MEDIATOKEN_API, params=params,
headers=auth_header)
jsonres = self._session.http.json(res,
schema=self._MEDIATOKEN_SCHEMA)
mediatoken = jsonres["token"]
res = self._session.http.post(self._LICENSE_API,
params={"t": mediatoken},
json={"kv": "a", "lt": ticket})
jsonres = self._session.http.json(res,
schema=self._LICENSE_SCHEMA)
cid = jsonres["cid"]
k = jsonres["k"]
res = sum(self.STRTABLE.find(k[i]) * (58 ** (len(k) - 1 - i)) for i in range(len(k)))
encvideokey = struct.pack(">QQ", res >> 64, res & 0xffffffffffffffff)
# HKEY:
# RC4KEY = unhexlify('DB98A8E7CECA3424D975280F90BD03EE')
# RC4DATA = unhexlify(b'D4B718BBBA9CFB7D0192A58F9E2D146A'
# b'FC5DB29E4352DE05FC4CF2C1005804BB')
# rc4 = ARC4.new(RC4KEY)
# HKEY = rc4.decrypt(RC4DATA)
h = hmac.new(unhexlify(self.HKEY),
(cid + self.deviceid).encode("utf-8"),
digestmod=hashlib.sha256)
enckey = h.digest()
aes = AES.new(enckey, AES.MODE_ECB)
return aes.decrypt(encvideokey)
def send(self, request, stream=False, timeout=None, verify=True, cert=None,
proxies=None):
resp = Response()
resp.status_code = 200
ticket = re.findall(r"abematv-license://(.*)", request.url)[0]
resp._content = self._get_videokey_from_ticket(ticket)
return resp
def close(self):
return
@pluginmatcher(re.compile(r"""
https?://abema\.tv/(
now-on-air/(?P<onair>[^?]+)
|
video/episode/(?P<episode>[^?]+)
|
channels/.+?/slots/(?P<slots>[^?]+)
)
""", re.VERBOSE))
class AbemaTV(Plugin):
_CHANNEL = "https://api.abema.io/v1/channels"
_USER_API = "https://api.abema.io/v1/users"
_PRGM_API = "https://api.abema.io/v1/video/programs/{0}"
_SLOTS_API = "https://api.abema.io/v1/media/slots/{0}"
_PRGM3U8 = "https://vod-abematv.akamaized.net/program/{0}/playlist.m3u8"
_SLOTM3U8 = "https://vod-abematv.akamaized.net/slot/{0}/playlist.m3u8"
SECRETKEY = (
b"v+Gjs=25Aw5erR!J8ZuvRrCx*rGswhB&qdHd_SYerEWdU&a?3DzN9B"
+ b"Rbp5KwY4hEmcj5#fykMjJ=AuWz5GSMY-d@H7DMEh3M@9n2G552Us$$"
+ b"k9cD=3TxwWe86!x#Zyhe"
)
_USER_SCHEMA = validate.Schema({"profile": {"userId": str}, "token": str})
_CHANNEL_SCHEMA = validate.Schema({
"channels": [{
"id": str,
"name": str,
"playback": {
validate.optional("dash"): str,
"hls": str,
},
}],
})
_PRGM_SCHEMA = validate.Schema({"terms": [{validate.optional("onDemandType"): int}]})
_SLOT_SCHEMA = validate.Schema({"slot": {"flags": {validate.optional("timeshiftFree"): bool}}})
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.session.http.headers.update({"User-Agent": useragents.CHROME})
def _generate_applicationkeysecret(self, deviceid):
deviceid = deviceid.encode("utf-8") # for python3
# plus 1 hour and drop minute and secs
# for python3 : floor division
ts_1hour = (int(time.time()) + 60 * 60) // 3600 * 3600
time_struct = time.gmtime(ts_1hour)
ts_1hour_str = str(ts_1hour).encode("utf-8")
h = hmac.new(self.SECRETKEY, digestmod=hashlib.sha256)
h.update(self.SECRETKEY)
tmp = h.digest()
for _ in range(time_struct.tm_mon):
h = hmac.new(self.SECRETKEY, digestmod=hashlib.sha256)
h.update(tmp)
tmp = h.digest()
h = hmac.new(self.SECRETKEY, digestmod=hashlib.sha256)
h.update(urlsafe_b64encode(tmp).rstrip(b"=") + deviceid)
tmp = h.digest()
for _ in range(time_struct.tm_mday % 5):
h = hmac.new(self.SECRETKEY, digestmod=hashlib.sha256)
h.update(tmp)
tmp = h.digest()
h = hmac.new(self.SECRETKEY, digestmod=hashlib.sha256)
h.update(urlsafe_b64encode(tmp).rstrip(b"=") + ts_1hour_str)
tmp = h.digest()
for _ in range(time_struct.tm_hour % 5): # utc hour
h = hmac.new(self.SECRETKEY, digestmod=hashlib.sha256)
h.update(tmp)
tmp = h.digest()
return urlsafe_b64encode(tmp).rstrip(b"=").decode("utf-8")
def _is_playable(self, vtype, vid):
auth_header = {"Authorization": f"Bearer {self.usertoken}"}
if vtype == "episode":
res = self.session.http.get(self._PRGM_API.format(vid),
headers=auth_header)
jsonres = self.session.http.json(res, schema=self._PRGM_SCHEMA)
playable = False
for item in jsonres["terms"]:
if item.get("onDemandType", False) == 3:
playable = True
return playable
elif vtype == "slots":
res = self.session.http.get(self._SLOTS_API.format(vid),
headers=auth_header)
jsonres = self.session.http.json(res, schema=self._SLOT_SCHEMA)
return jsonres["slot"]["flags"].get("timeshiftFree", False) is True
def _get_streams(self):
deviceid = str(uuid.uuid4())
appkeysecret = self._generate_applicationkeysecret(deviceid)
json_data = {"deviceId": deviceid,
"applicationKeySecret": appkeysecret}
res = self.session.http.post(self._USER_API, json=json_data)
jsonres = self.session.http.json(res, schema=self._USER_SCHEMA)
self.usertoken = jsonres["token"] # for authorzation
matchresult = self.match
if matchresult.group("onair"):
onair = matchresult.group("onair")
if onair == "news-global":
self._CHANNEL = update_qsd(self._CHANNEL, {"division": "1"})
res = self.session.http.get(self._CHANNEL)
jsonres = self.session.http.json(res, schema=self._CHANNEL_SCHEMA)
channels = jsonres["channels"]
for channel in channels:
if onair == channel["id"]:
break
else:
raise NoStreamsError
playlisturl = channel["playback"]["hls"]
elif matchresult.group("episode"):
episode = matchresult.group("episode")
if not self._is_playable("episode", episode):
log.error("Premium stream is not playable")
return {}
playlisturl = self._PRGM3U8.format(episode)
elif matchresult.group("slots"):
slots = matchresult.group("slots")
if not self._is_playable("slots", slots):
log.error("Premium stream is not playable")
return {}
playlisturl = self._SLOTM3U8.format(slots)
log.debug("URL={0}".format(playlisturl))
# hook abematv private protocol
self.session.http.mount("abematv-license://",
AbemaTVLicenseAdapter(self.session, deviceid,
self.usertoken))
return AbemaTVHLSStream.parse_variant_playlist(self.session, playlisturl)
__plugin__ = AbemaTV