#!/usr/bin/python3
import sys
import os
import o11
import json
import datetime
import pytz
import base64
import jwt
import pkce
from urllib import parse
from Crypto.Cipher import AES
from binascii import hexlify
from cryptography.hazmat.primitives import hashes
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 = '/SwitchTV_' + user + '.tokens'
user_agent = 'Dalvik/2.1.0 (Linux; U; Android 9; SM-G9650 Build/PPR1.180610.011)'

def deriveKey(password):
    bytes_data = b''
    last = b''
    salt = os.urandom(8)
    while len(bytes_data) < 32:
        md5 = hashes.Hash(hashes.MD5())
        md5.update(last + password.encode() + salt)
        last = md5.finalize()
        bytes_data += last
    return bytes_data, salt

def pad(data):
    pad_length = AES.block_size - (len(data) % AES.block_size)
    return data + bytes([pad_length]) * pad_length

def aes_encrypt(plaintext, key, iv):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    ciphertext = cipher.encrypt(pad(plaintext.encode()))
    return ciphertext

def encrypt(passphrase, plaintext):
    key, salt = deriveKey(passphrase)
    iv = os.urandom(16)
    ciphertext = aes_encrypt(plaintext, key, iv)
    return '{"ct":"%s","iv":"%s","s":"%s"}' % (base64.b64encode(ciphertext).decode('utf-8'), hexlify(iv).decode("utf8"), hexlify(salt).decode("utf8"))

def encode_credentials(creds):
    ciphertext = encrypt('SwitchTV2020', creds)
    b64_ciphertext = base64.b64encode(ciphertext.encode())
    return b64_ciphertext.decode()

def do_cdm_external(pssh_data, blob, 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)
    blob_parts = blob.split('&')
    lic_headers = {'User-Agent': user_agent, 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token}
    data = {'contentID': blob_parts[1].split('AssetId=')[-1], 'contentType': '2', 'authorizationToken': blob_parts[2].split('AuthToken=')[-1], 'licenseChallenge': base64.b64encode(challenge_data).decode()}
    licence = req.post('https://sg.tv.etisalat.ae:9443/vgemultidrm/v1/widevine/license', headers=lic_headers, json=data)
    l_data = licence.json()
    cdm_obj.parse_license(session_id, base64.b64decode(l_data['licenseData'][0]))
    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 get_pssh_from_mpd(url):
    url = url.replace('http', 'https')
    headers = {'User-Agent': user_agent}
    response = req.get(url, headers=headers)
    for cp in BeautifulSoup(response.content, features="xml").findAll('ContentProtection'):
        if cp.get('schemeIdUri', '').lower() == 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
            pssh_tag = cp.find('cenc:pssh')
            if pssh_tag:
                return pssh_tag.text
    return None

def get_channels(token):
    headers = {'Accept': 'application/json, text/plain, */*', 'Authorization': 'Bearer ' + token, 'Origin': 'https://www.switchtv.ae', 'Referer': 'https://www.switchtv.ae/', 'User-Agent': user_agent}
    response = req.get('https://sg.tv.etisalat.ae:9443/ctap/r1.3.0/channels', headers=headers)
    return response.json().get('channels', [])

def get_single(token, channel_id):
    headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': user_agent, 'Accept-Encoding': 'gzip'}
    response = req.post(f'https://sg.tv.etisalat.ae:9443/ctap/r1.6.0/devices/me/playsessions?channelId={channel_id}', headers=headers)
    data = response.json()
    return data.get('_links', {}).get('playUrl', {}).get('href', ''), data.get('drmProperties', {}).get('blob', '')

def do_token_refresh(refresh_token):
    headers = {'Content-Type': 'application/json', 'User-Agent': user_agent, 'Accept-Encoding': 'gzip'}
    response = req.post('https://sg.tv.etisalat.ae:9443/oauth2/token?grant_type=refresh_token&refresh_token=' + refresh_token, headers=headers)
    data = response.json()
    return data.get('access_token', ''), data.get('refresh_token', '')

def get_csrf():
    headers = {'User-Agent': 'Mozilla/5.0 (Linux; Android 9; SM-G9650 Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/121.0.6167.143 Mobile Safari/537.36', 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Referer': 'https://iframes.bsstv.etisalat.ae/signin'}
    response = req.get('https://iframes.bsstv.etisalat.ae/refreshtoken', headers=headers)
    return response.json().get('token', '')

def init(code_challenge):
    headers = {'User-Agent': 'Mozilla/5.0 (Linux; Android 9; SM-G9650 Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/121.0.6167.143 Mobile Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'X-Requested-With': 'ae.etisalat.switchtv', 'Referer': 'https://iframes.bsstv.etisalat.ae/', 'Accept-Encoding': 'gzip, deflate, br'}
    params = {'response_type': 'code', 'client_id': '02.SWITCHTV-Android-2.37cef4d3-1f2f-4ea6-bfcc-9605930e328f.793ea305573e263f', 'state': 'testState', 'code_challenge_method': 'S256', 'ui_locales': 'eng', 'code_challenge': code_challenge, 'redirect_uri': 'https://localhost:3345/'}
    response = req.get('https://sg.tv.etisalat.ae:9443/oauth2/authorize', params=params, headers=headers)
    state = parse.parse_qs(parse.urlparse(response.url).query).get('state', [''])[0]
    return state, response.url

def do_login_request(state, username, pwd):
    headers = {'X-CSRF-TOKEN': get_csrf(), 'User-Agent': 'Mozilla/5.0 (Linux; Android 9; SM-G9650 Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/121.0.6167.143 Mobile Safari/537.36', 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Origin': 'https://iframes.bsstv.etisalat.ae', 'Referer': 'https://iframes.bsstv.etisalat.ae/signin', 'Accept-Encoding': 'gzip, deflate, br'}
    decoded = jwt.decode(state, options={"verify_signature": False})
    creds = json.dumps({"userId": username, "password": pwd})
    data = {"data": encode_credentials(creds), "device_type": "Android", "nonce": decoded['nonce']}
    response = req.post('https://iframes.bsstv.etisalat.ae/signin', headers=headers, json=data)
    return response.json().get('sessionToken', '')

def accountpromo(state, sess):
    headers = {'User-Agent': 'Mozilla/5.0 (Linux; Android 9; SM-G9650 Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/121.0.6167.143 Mobile Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'X-Requested-With': 'ae.etisalat.switchtv', 'Referer': 'https://iframes.bsstv.etisalat.ae/', 'Accept-Encoding': 'gzip, deflate, br'}
    decoded = jwt.decode(state, options={"verify_signature": False})
    params = {'https://iframes.bsstv.etisalat.ae/signinresponse_type': 'code', 'client_id': 'cisco_ivp', 'scope': 'openid', 'state': state, 'nonce': decoded['nonce'], 'prompt': 'login', 'device_type': 'Android', 'redirect_uri': 'https://sg.tv.etisalat.ae:9443/oauth2/authorizeEnd', 'response_code': '1', 'response_status': 'success', 'sessionToken': sess}
    response = req.get('https://iframes.bsstv.etisalat.ae/accountpromo', params=params, headers=headers, allow_redirects=False)
    loc = response.headers.get('Location', '')
    response = req.get(loc, headers=headers, allow_redirects=False)
    code = parse.parse_qs(parse.urlparse(response.headers.get('Location', '')).query).get('code', [''])[0]
    return code

def oauth_token(code, code_verifier):
    headers = {'Content-Type': 'application/json', 'User-Agent': user_agent, 'Accept-Encoding': 'gzip'}
    params = {'grant_type': 'authorization_code', 'code': code, 'redirect_uri': 'https://localhost:3345/', 'client_id': '02.SWITCHTV-Android-2.37cef4d3-1f2f-4ea6-bfcc-9605930e328f.793ea305573e263f', 'code_verifier': code_verifier}
    response = req.post('https://sg.tv.etisalat.ae:9443/oauth2/token', headers=headers, params=params, allow_redirects=False)
    data = response.json()
    return data.get('refresh_token', ''), data.get('access_token', '')

def login():
    print("logging in...", file=sys.stderr)
    code_verifier, code_challenge = pkce.generate_pkce_pair()
    state, ref_url = init(code_challenge)
    sess = do_login_request(state, user, password)
    code = accountpromo(state, sess)
    refresh_token, token = oauth_token(code, code_verifier)
    auth_data = {'refresh_token': refresh_token}
    json.dump(auth_data, open(os.path.abspath(os.path.dirname(__file__)) + authFile, 'w'))
    print("logged in successfully", file=sys.stderr)

def get_token():
    auth = json.load(open(os.path.abspath(os.path.dirname(__file__)) + authFile))
    token, refresh_token = do_token_refresh(auth['refresh_token'])
    auth['refresh_token'] = refresh_token
    json.dump(auth, open(os.path.abspath(os.path.dirname(__file__)) + authFile, 'w'))
    return token

def do_action():
    if action == "login":
        login()
        sys.exit()
    try:
        token = get_token()
    except:
        return "error"

    if action == "channels":
        output = {'Channels': []}
        for c in get_channels(token):
            output['Channels'].append({'Name': c['name'], 'Mode': "live", 'SessionManifest': True, 'ManifestScript': 'id=' + c['id'], 'CdmType': "widevine", 'UseCdm': True, 'Cdm': 'id=' + c['id'], 'Video': 'best'})
        print(json.dumps(output, indent=2))
    elif action == "events":
        output = {'Events': []}
        for c in get_channels(token):
            output['Events'].append({'Name': c['name'], 'Mode': "live", 'SessionManifest': True, 'ManifestScript': 'id=' + c['id'], 'CdmType': "widevine", 'UseCdm': True, 'Cdm': 'id=' + c['id'], '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":
        channel_id = id.replace('id=', '') if id.startswith('id=') else id
        url, blob = get_single(token, channel_id)
        pssh_data = get_pssh_from_mpd(url)
        output = {"Cdn": [], "ManifestUrl": url, "Headers": {"Manifest": {'User-Agent': user_agent}, "Media": {'User-Agent': user_agent}}, "Blob": blob, "Token": token}
        if pssh_data:
            output['Pssh'] = pssh_data
        print(json.dumps(output))
    elif action == "cdm" and cdm_param == "external":
        channel_id = id.replace('id=', '') if id.startswith('id=') else id
        url, blob = get_single(token, channel_id)
        pssh_data = get_pssh_from_mpd(url)
        if pssh_data and blob:
            for key in do_cdm_external(pssh_data, blob, token):
                print(key)

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