#!/usr/bin/python3
import sys
import os
import o11
import json
import datetime
import pytz
import base64
import hmac
import hashlib
import uuid
import random
import string
import time
import jwt
from urllib import parse
from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH
from bs4 import BeautifulSoup

WVD_PATH = './WVD.wvd'

user = o11.parse_params(sys.argv, 'user')
password = o11.parse_params(sys.argv, 'password')

id = o11.parse_params(sys.argv, 'id')
action = o11.parse_params(sys.argv, 'action')

bind = o11.parse_params(sys.argv, 'bind')
proxy = o11.parse_params(sys.argv, 'proxy')
doh = o11.parse_params(sys.argv, 'doh')
worker = o11.parse_params(sys.argv, 'worker')

cdm_param = o11.parse_params(sys.argv, 'cdm')
challenge = o11.parse_params(sys.argv, 'challenge')

o11Session = o11.session(bind=bind, proxy=proxy, worker=worker)
req = o11Session.get_session()
if doh != "":
    o11.dns(doh)

if challenge == "cert":
    challenge = "CAQ="

authFile = '/Movistar_' + user + '.tokens'
user_agent = 'okhttp/5.0.0-alpha.11'
device_session_type = 'phone.android'

def do_cdm_external(pssh_data, ssp_token):
    pssh_obj = PSSH(pssh_data)
    device_obj = Device.load(WVD_PATH)
    cdm_obj = Cdm.from_device(device_obj)
    session_id = cdm_obj.open()
    challenge_data = cdm_obj.get_license_challenge(session_id, pssh_obj)
    lic_headers = {'nv-authorizations': ssp_token, 'Content-Type': 'application/octet-stream', 'User-Agent': 'Exoplayer-Android-Movilidad', 'Connection': 'Keep-Alive', 'Accept-Encoding': 'gzip'}
    licence = req.post('https://wv-ottlic-f3.imagenio.telefonica.net/TFAESP/wvls/contentlicenseservice/v1/licenses', headers=lic_headers, data=challenge_data)
    cdm_obj.parse_license(session_id, licence.content)
    keys = [f"{key.kid.hex}:{key.key.hex()}" for key in cdm_obj.get_keys(session_id) if key.type != 'SIGNING']
    cdm_obj.close(session_id)
    return keys

def find_wv_pssh_offsets(raw):
    offsets = []
    offset = 0
    while True:
        offset = raw.find(b'pssh', offset)
        if offset == -1:
            break
        size = int.from_bytes(raw[offset-4:offset], byteorder='big')
        pssh_offset = offset - 4
        offsets.append(raw[pssh_offset:pssh_offset+size])
        offset += size
    return offsets

def to_pssh(content):
    wv_offsets = find_wv_pssh_offsets(content)
    return [base64.b64encode(wv_offset).decode() for wv_offset in wv_offsets]

def init_to_pssh(init_url, tcdn_token):
    headers = {'Accept': '*/*', 'Origin': 'null', 'Referer': 'https://ver.movistarplus.es/', 'User-Agent': user_agent, 'X-Tcdn-Token': tcdn_token}
    response = req.get(init_url, headers=headers)
    return to_pssh(response.content)

def get_pssh_from_mpd(url, tcdn_token):
    headers = {'Accept': '*/*', 'Origin': 'null', 'Referer': 'https://ver.movistarplus.es/', 'User-Agent': user_agent, 'X-Tcdn-Token': tcdn_token}
    response = req.get(url, headers=headers)
    location = response.url
    soup = BeautifulSoup(response.content, features="xml")
    for cp in soup.find_all('ContentProtection'):
        if cp.get('schemeIdUri', '').lower() == 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
            pssh = cp.find('cenc:pssh')
            if pssh:
                return pssh.text
    init = soup.find('SegmentTemplate')['initialization']
    bandwidth = soup.find('Representation')['bandwidth']
    rep_id = soup.find('Representation')['id']
    loc_parts = location.split('/')
    loc_parts.pop()
    init_url = '/'.join(loc_parts) + '/' + init.replace('$Bandwidth$', bandwidth).replace('$RepresentationID$', rep_id)
    psshs = init_to_pssh(init_url, tcdn_token)
    return min(psshs) if psshs else None

def get_channels():
    headers = {'Accept': 'application/json, text/javascript, */*; q=0.01', 'Origin': 'https://ver.movistarplus.es', 'Referer': 'https://ver.movistarplus.es/', 'User-Agent': user_agent}
    params = {'showNonRated': 'true', 'tlsstream': 'true', 'demarcation': '1', 'version': '8.1'}
    response = req.get(f'https://ottcache.dof6.com/movistarplus/{device_session_type}/OTT/contents/epg', params=params, headers=headers)
    return response.json()

def generate_random_string(length=9):
    characters = string.ascii_letters + string.digits
    return ''.join(random.choice(characters) for _ in range(length))

def hmac_sha1(input_str):
    raw = input_str.encode("utf-8")
    key = 'M9p1T3KBgSWFdGtVqUZRnjtPodXiPeiw'.encode('utf-8')
    hashed = hmac.new(key, raw, hashlib.sha1)
    return base64.encodebytes(hashed.digest()).decode('utf-8').strip()

def get_signature(method, url, token, params, data):
    hmac_params = {'consumer_key': 'phone_android_2507', 'nonce': generate_random_string(), 'signature_method': 'HMAC-SHA1', 'timestamp': round(time.time()), 'version': '1.0'}
    endpoint = url.split('?')[0].replace('https://', '')
    input_json = {}
    input_json.update(params)
    input_json.update(data)
    input_json.update(hmac_params)
    sorted_dict = {key: value for key, value in sorted(input_json.items())}
    input_str = ''
    for key, value in sorted_dict.items():
        if key in ['username', 'password']:
            value = parse.quote(value)
        input_str += f'&{key}={value}'
    input_str = f'{method}&{parse.quote_plus(endpoint)}&{parse.quote_plus(token)}' + parse.quote_plus(input_str)
    signature = hmac_sha1(input_str)
    return base64.b64encode(f'consumer_key={hmac_params["consumer_key"]},nonce={hmac_params["nonce"]},signature_method={hmac_params["signature_method"]},signature={signature},timestamp={hmac_params["timestamp"]},version={hmac_params["version"]}'.encode('utf-8')).decode('utf8')

def do_login_request(username, pwd):
    url = 'https://soter-pf.sve.video.telefonicaservices.com/service/oidc/connect/token'
    token = f'Basic={base64.b64encode((str(uuid.uuid4()) + f":{device_session_type}").encode("utf-8")).decode("utf-8")}'
    headers = {'x-signature': 'true', 'Content-Type': 'application/x-www-form-urlencoded', 'Accept-Encoding': 'gzip', 'User-Agent': user_agent}
    params = {'deviceClass': device_session_type}
    data = {'password': pwd, 'grant_type': 'password', 'scope': 'api', 'client_secret': 'secret', 'client_id': '491aafabb70d4644954d8fb21e0b93b1', 'username': username}
    signature = get_signature('POST', url, token, params, data)
    headers['Authorization'] = f'OPPlus {token}, Signature={signature}'
    response = req.post(url, headers=headers, params=params, data=data)
    return response.json().get('access_token', '')

def get_account_info(token):
    url = f'https://soter-pf.sve.video.telefonicaservices.com/service/login/{device_session_type}/accountInfo'
    token_str = f'Bearer={token}'
    headers = {'x-signature': 'true', 'Content-Type': 'application/json;charset=UTF-8', 'User-Agent': user_agent}
    params = {'legacy': 'true'}
    signature = get_signature('GET', url, token_str, params, {})
    headers['Authorization'] = f'OPPlus {token_str}, Signature={signature}'
    response = req.get(url, params=params, headers=headers)
    return response.json().get('ofertas', [{}])[0].get('accountNumber', '')

def enrol_device(token, account_number):
    decoded = jwt.decode(token, options={"verify_signature": False})
    url = 'https://soter-pf.sve.video.telefonicaservices.com/service/login/devices'
    token_str = f'Bearer={token}'
    headers = {'x-signature': 'true', 'Content-Type': 'application/json;charset=UTF-8', 'User-Agent': user_agent}
    params = {'accountId': account_number, 'deviceId': decoded['did'], 'deviceType': device_session_type, 'legacy': 'true'}
    signature = get_signature('PUT', url, token_str, params, {})
    headers['Authorization'] = f'OPPlus {token_str}, Signature={signature}'
    req.put(url, params=params, headers=headers)

def init_data(token, account_number):
    decoded = jwt.decode(token, options={"verify_signature": False})
    url = 'https://soter-pf.sve.video.telefonicaservices.com/service/login/initData'
    token_str = f'Bearer={token}'
    headers = {'CMP_InitData': 'true', 'x-movistarplus-app': '9.7.8_489', 'x-movistarplus-ui': 'n', 'x-movistarplus-os': 'Android_9', 'x-movistarplus-deviceid': decoded['did'], 'x-movistarplus-servG': 'true', 'x-movistarplus-dman': 'samsung', 'x-signature': 'true', 'Content-Type': 'application/json', 'User-Agent': user_agent}
    params = {'legacy': 'true', 'status': 'login'}
    data = {'accountNumber': account_number, 'sessionUserProfile': 0, 'deviceType': 'ANS_OTT', 'isKidProfile': False, 'streamFormat': 'DASH', 'streamDRM': 'Widevine', 'streamMiscellanea': 'HTTPS', 'deviceManufacturerProduct': 'Android', 'deviceId': decoded['did']}
    signature = get_signature('POST', url, token_str, params, {})
    headers['Authorization'] = f'OPPlus {token_str}, Signature={signature}'
    response = req.post(url, headers=headers, params=params, json=data)
    resp_data = response.json()
    return resp_data.get('accessToken', ''), resp_data.get('sspToken', ''), resp_data.get('legacyAccessToken', '')

def delete_session(token, account_number):
    url = f'https://soter-pf.sve.video.telefonicaservices.com/service/session/{device_session_type}/{account_number}/session/0'
    token_str = f'Bearer={token}'
    headers = {'x-signature': 'true', 'User-Agent': user_agent}
    signature = get_signature('DELETE', url, token_str, {}, {})
    headers['Authorization'] = f'OPPlus {token_str}, Signature={signature}'
    req.delete(url, headers=headers)

def refresh_tcdn(hzid_token, account_number):
    headers = {'Authorization': 'Bearer ' + hzid_token, 'x-signature': 'true', 'Content-Type': 'application/json;charset=UTF-8', 'User-Agent': user_agent}
    response = req.post(f'https://idserver.dof6.com/{account_number}/devices/{device_session_type}/cdn/token/refresh', headers=headers)
    return response.json().get('access_token', '')

def login():
    print("logging in...", file=sys.stderr)
    token = do_login_request(user, password)
    account_number = get_account_info(token)
    enrol_device(token, account_number)
    access_token, ssp_token, hzid_token = init_data(token, account_number)
    auth_data = {'access_token': access_token, 'account_number': account_number, 'ssp_token': ssp_token, 'hzid_token': hzid_token}
    json.dump(auth_data, open(os.path.abspath(os.path.dirname(__file__)) + authFile, 'w'))
    print("logged in successfully", file=sys.stderr)

def get_auth():
    auth = json.load(open(os.path.abspath(os.path.dirname(__file__)) + authFile))
    return auth['access_token'], auth['account_number'], auth['ssp_token'], auth['hzid_token']

def do_action():
    if action == "login":
        login()
        sys.exit()
    try:
        access_token, account_number, ssp_token, hzid_token = get_auth()
    except:
        return "error"

    if action == "channels":
        output = {'Channels': []}
        for c in get_channels():
            output['Channels'].append({'Name': c['Nombre'], 'Mode': "live", 'SessionManifest': True, 'ManifestScript': 'url=' + c['PuntoReproduccion'], 'CdmType': "widevine", 'UseCdm': True, 'Cdm': 'url=' + c['PuntoReproduccion'], 'Video': 'best'})
        print(json.dumps(output, indent=2))
    elif action == "events":
        output = {'Events': []}
        for c in get_channels():
            output['Events'].append({'Name': c['Nombre'], 'Mode': "live", 'SessionManifest': True, 'ManifestScript': 'url=' + c['PuntoReproduccion'], 'CdmType': "widevine", 'UseCdm': True, 'Cdm': 'url=' + c['PuntoReproduccion'], 'Video': 'best', 'Autostart': True, 'Start': int(datetime.datetime.now(pytz.UTC).timestamp()), 'End': int((datetime.datetime.now(pytz.UTC) + datetime.timedelta(hours=4)).timestamp())})
        print(json.dumps(output, indent=2))
    elif action == "manifest":
        url = id.replace('url=', '') if id.startswith('url=') else id
        tcdn_token = refresh_tcdn(hzid_token, account_number)
        pssh_data = get_pssh_from_mpd(url, tcdn_token)
        delete_session(access_token, account_number)
        output = {"Cdn": [], "ManifestUrl": url, "Headers": {"Manifest": {'User-Agent': user_agent, 'X-Tcdn-Token': tcdn_token}, "Media": {'User-Agent': user_agent, 'X-Tcdn-Token': tcdn_token}}, "TcdnToken": tcdn_token, "SspToken": ssp_token}
        if pssh_data:
            output['Pssh'] = pssh_data
        print(json.dumps(output))
    elif action == "cdm" and cdm_param == "external":
        url = id.replace('url=', '') if id.startswith('url=') else id
        tcdn_token = refresh_tcdn(hzid_token, account_number)
        pssh_data = get_pssh_from_mpd(url, tcdn_token)
        delete_session(access_token, account_number)
        if pssh_data:
            for key in do_cdm_external(pssh_data, ssp_token):
                print(key)

if do_action() == "error":
    login()
    do_action()
