#!/usr/bin/python3
import sys
import os
import o11
import base64
import json
import datetime
import pytz
import jwt
import uuid
import secrets
from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH
from bs4 import BeautifulSoup

try:
    from curl_cffi import requests as curl_requests
    USE_CURL = True
except ImportError:
    import requests as curl_requests
    USE_CURL = False

WVD_PATH = './WVD.wvd'

user = o11.parse_params(sys.argv, 'user')
password = o11.parse_params(sys.argv, 'password')
token_param = o11.parse_params(sys.argv, 'token')
pin_param = o11.parse_params(sys.argv, 'pin')

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 = o11.parse_params(sys.argv, 'cdm')
drm = o11.parse_params(sys.argv, 'drm')
kid = o11.parse_params(sys.argv, 'kid')
pssh = o11.parse_params(sys.argv, 'pssh')
challenge = o11.parse_params(sys.argv, 'challenge')

heartbeaturl = o11.parse_params(sys.argv, 'heartbeaturl')
heartbeatparams = o11.parse_params(sys.argv, 'heartbeatparams')

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

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

authFile = '/DaznV23_' + user + '.tokens'
proxies = {"http": proxy, "https": proxy} if proxy else None

user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'

def curl_get(url, **kwargs):
    if USE_CURL:
        return curl_requests.get(url, proxies=proxies, impersonate="chrome", **kwargs)
    return req.get(url, **kwargs)

def curl_post(url, **kwargs):
    if USE_CURL:
        return curl_requests.post(url, proxies=proxies, impersonate="chrome", **kwargs)
    return req.post(url, **kwargs)

def curl_delete(url, **kwargs):
    if USE_CURL:
        return curl_requests.delete(url, proxies=proxies, impersonate="chrome", **kwargs)
    return req.delete(url, **kwargs)

def do_cdm_external(pssh_data, license_url, token):
    """Extract Widevine keys using pywidevine library"""
    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 = {
        'authorization': 'Bearer ' + token,
        'content-type': 'application/octet-stream',
        'origin': 'https://www.dazn.com',
        'referer': 'https://www.dazn.com/',
        'user-agent': user_agent,
        'x-correlation-id': str(uuid.uuid4())
    }

    licence = curl_post(license_url, headers=lic_headers, data=challenge_data)

    try:
        licence.raise_for_status()
        cdm_obj.parse_license(session_id, licence.content)
        keys = []
        for key in cdm_obj.get_keys(session_id):
            if key.type != 'SIGNING':
                keys.append(f"{key.kid.hex}:{key.key.hex()}")
        cdm_obj.close(session_id)
        return keys
    except Exception as e:
        print(f'License request failed: {e}', file=sys.stderr)
        return []

def get_pssh_from_mpd(url, cdn_token_name, cdn_token):
    headers = {'user-agent': user_agent, cdn_token_name: cdn_token}
    response = curl_get(url, headers=headers)
    
    if response.status_code != 200:
        url += f'&{cdn_token_name}={cdn_token}'
        response = curl_get(url, headers=headers)
    
    content_protections = BeautifulSoup(response.content, features="xml").find_all('ContentProtection')
    pssh_val = None
    for cp in content_protections:
        if cp.get('schemeIdUri', '').lower() == 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
            pssh_val = cp.text.strip()
    
    final_url = response.url if cdn_token_name not in response.url else url
    return pssh_val, final_url

def get_version():
    headers = {'accept': '*/*', 'origin': 'https://www.dazn.com', 'referer': 'https://www.dazn.com/', 'user-agent': user_agent}
    response = curl_get('https://pkg.fe.indazn.com/@dazn/peng-html5-core/live-production/web/it/0/version.json', headers=headers)
    return response.json().get('version', '4.53.8.24300')

def do_refresh(token):
    headers = {
        'authorization': 'Bearer ' + token, 'content-type': 'application/json',
        'origin': 'https://www.dazn.com', 'referer': 'https://www.dazn.com/', 'user-agent': user_agent,
    }
    decoded = jwt.decode(token, options={"verify_signature": False})
    device_id = decoded['deviceId']
    json_data = {'DeviceId': device_id.split('-')[-1]}
    response = curl_post('https://ott-authz-bff-prod.ar.indazn.com/v5/RefreshAccessToken', headers=headers, json=json_data)
    data = response.json()
    return data['AuthToken']['Token']

def do_login_request(username, pwd):
    headers = {
        'content-type': 'application/json', 'origin': 'https://www.dazn.com',
        'referer': 'https://www.dazn.com/', 'user-agent': user_agent,
        'x-dazn-ua': user_agent + 'signin/4.53.8.24300 hyper/0.14.0 (web; production; en)',
    }
    json_data = {'Email': username, 'Password': pwd, 'Platform': 'web', 'DeviceId': secrets.token_hex(5)}
    response = curl_post('https://authentication-prod.ar.indazn.com/v5/SignIn', headers=headers, json=json_data)
    return response.json()['AuthToken']['Token']

def get_single(token, asset_id, pin=None, version='4.53.8.24300'):
    decoded = jwt.decode(token, options={"verify_signature": False})
    headers = {
        'accept': '*/*', 'authorization': 'Bearer ' + token, 'origin': 'https://www.dazn.com',
        'referer': 'https://www.dazn.com/', 'user-agent': user_agent,
        'x-dazn-device': decoded['deviceId'], 'x-correlation-id': str(uuid.uuid4())
    }
    if pin:
        headers['x-age-verification-pin'] = pin
    params = {
        'AppVersion': version, 'DrmType': 'WIDEVINE', 'Format': 'MPEG-DASH',
        'PlayerId': '@dazn/peng-html5-core/web/web', 'Platform': 'web', 'LanguageCode': 'en',
        'Model': 'unknown', 'Secure': 'true', 'Manufacturer': 'microsoft',
        'PlayReadyInitiator': 'false', 'Capabilities': 'mta', 'AssetId': asset_id, 'MtaLanguageCode': ''
    }
    response = curl_get('https://api.playback.indazn.com/v5/Playback', params=params, headers=headers)
    data = response.json()
    playback_details = data['PlaybackDetails'][0]
    cdn_token = playback_details['CdnToken']
    return playback_details["ManifestUrl"], cdn_token["Name"], cdn_token["Value"], playback_details['LaUrl'], data['PlaybackLock']['LockId']

def delete_concurrency(token, lock_id):
    headers = {
        'authorization': 'Bearer ' + token, 'origin': 'https://www.dazn.com',
        'referer': 'https://www.dazn.com/', 'user-agent': user_agent, 'x-correlation-id': str(uuid.uuid4())
    }
    try:
        curl_delete('https://concurrency-v2.playback.indazn.com/v2/concurrency/lock/' + lock_id, headers=headers, timeout=15)
    except:
        pass

def get_rails(token):
    headers = {'accept': 'application/json, text/plain, */*', 'content-type': 'application/json', 'origin': 'https://www.dazn.com', 'referer': 'https://www.dazn.com/', 'user-agent': user_agent}
    decoded = jwt.decode(token, options={"verify_signature": False})
    entitlements = ','.join([e['id'] for e in decoded['entitlements']['entitlementSets']])
    params = {'groupId': 'home', 'country': decoded['contentCountry'], 'openBrowse': 'false', 'userEntitlements': entitlements}
    response = curl_get('https://rails.discovery.indazn.com/eu/v8/rails', headers=headers, params=params)
    return response.json().get('Rails', [])

def get_channel_rails(token):
    headers = {'accept': 'application/json, text/plain, */*', 'content-type': 'application/json', 'origin': 'https://www.dazn.com', 'referer': 'https://www.dazn.com/', 'user-agent': user_agent}
    decoded = jwt.decode(token, options={"verify_signature": False})
    entitlements = ','.join([e['id'] for e in decoded['entitlements']['entitlementSets']])
    params = {'groupId': 'sport', 'country': decoded['contentCountry'], 'params': 'PageType:Sport;ContentType:Sport;ContentId:9kn3pow0we2r8hna2p0k4m2ff', 'openBrowse': 'false', 'userEntitlements': entitlements}
    response = curl_get('https://rails.discovery.indazn.com/eu/v8/rails', headers=headers, params=params)
    return response.json().get('Rails', [])

def single_rail(token, rail_id, rail_params):
    headers = {'accept': 'application/json, text/plain, */*', 'authorization': 'Bearer ' + token, 'content-type': 'application/json', 'origin': 'https://www.dazn.com', 'referer': 'https://www.dazn.com/', 'user-agent': user_agent}
    decoded = jwt.decode(token, options={"verify_signature": False})
    params = {'platform': 'web', 'id': rail_id, 'country': decoded['contentCountry'], 'languageCode': 'en', 'params': rail_params}
    response = curl_get('https://rail-router.discovery.indazn.com/eu/v6/Rail', headers=headers, params=params)
    data = response.json()
    return data.get('Tiles', [])

def get_channels(token):
    c = []
    try:
        for r in get_channel_rails(token):
            tiles = single_rail(token, r['Id'], r['Params'])
            for t in tiles:
                if t.get('Type') == 'Live':
                    c.append(t)
    except:
        pass
    try:
        for r in get_rails(token):
            tiles = single_rail(token, r['Id'], r['Params'])
            for t in tiles:
                if t.get('Type') == 'Live':
                    c.append(t)
    except:
        pass
    # Remove duplicates
    return list({each['Title']: each for each in c}.values())

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

def do_action():
    if action == "login":
        login()
        sys.exit()

    try:
        auth = json.load(open(os.path.abspath(os.path.dirname(__file__)) + authFile))
        token = do_refresh(auth['token'])
        auth['token'] = token
        json.dump(auth, open(os.path.abspath(os.path.dirname(__file__)) + authFile, 'w'))
        version = get_version()
    except:
        return "error"

    if action == "channels" or action == "events":
        output = {'Events': []} if action == "events" else {'Channels': []}
        channels = get_channels(token)
        for chan in channels:
            item = {
                'Name': chan['Title'].replace('/', '-'),
                'Mode': "live",
                'SessionManifest': True,
                'ManifestScript': 'id=' + str(chan['AssetId']),
                'CdmType': "widevine",
                'UseCdm': True,
                'Cdm': 'id=' + str(chan['AssetId']),
                'Video': 'best',
                'OnDemand': True,
                'SpeedUp': True
            }
            if action == "events":
                item['Autostart'] = True
                item['Start'] = int(datetime.datetime.now(pytz.UTC).timestamp())
                item['End'] = int((datetime.datetime.now(pytz.UTC) + datetime.timedelta(hours=4)).timestamp())
                output['Events'].append(item)
            else:
                output['Channels'].append(item)
        print(json.dumps(output, indent=2))

    elif action == "heartbeat":
        sys.exit()

    elif action == "manifest":
        asset_id = id
        pin = pin_param if pin_param else None
        try:
            url, cdn_token_name, cdn_token, license_url, lock_id = get_single(token, asset_id, pin, version)
            delete_concurrency(token, lock_id)
            pssh_data, final_url = get_pssh_from_mpd(url, cdn_token_name, cdn_token)
            
            output = {
                "Cdn": [],
                "ManifestUrl": final_url,
                "Headers": {"Manifest": {'User-Agent': user_agent, cdn_token_name: cdn_token}, "Media": {'User-Agent': user_agent, cdn_token_name: cdn_token}},
                "Heartbeat": {"Url": '', "Params": '', "PeriodMs": 5*60*1000},
                "LicenseUrl": license_url, "Token": token
            }
            if pssh_data:
                output['Pssh'] = pssh_data
            print(json.dumps(output))
        except Exception as e:
            print(f"Error getting manifest: {e}", file=sys.stderr)
            return "error"

    elif action == "cdm" and cdm == "internal":
        license_url = o11.parse_params(sys.argv, 'licenseUrl')
        tok = o11.parse_params(sys.argv, 'token') or token
        try:
            lic_headers = {'authorization': 'Bearer ' + tok, 'content-type': 'application/octet-stream', 'x-correlation-id': str(uuid.uuid4())}
            response = curl_post(license_url, headers=lic_headers, data=base64.b64decode(challenge))
            response_b64 = str(base64.b64encode(response.content), 'ascii')
            if response_b64.startswith('CA'):
                print(response_b64)
            else:
                print(response.text, file=sys.stderr)
                return "error"
        except Exception as e:
            print(f"Error: {e}", file=sys.stderr)
            return "error"

    elif action == "cdm" and cdm == "external":
        asset_id = id
        pin = pin_param if pin_param else None
        try:
            url, cdn_token_name, cdn_token, license_url, lock_id = get_single(token, asset_id, pin, version)
            delete_concurrency(token, lock_id)
            pssh_data, final_url = get_pssh_from_mpd(url, cdn_token_name, cdn_token)
            
            if pssh_data:
                keys = do_cdm_external(pssh_data, license_url, token)
                for key in keys:
                    print(key)
            else:
                print("No PSSH found in manifest", file=sys.stderr)
                return "error"
        except Exception as e:
            print(f"Error: {e}", file=sys.stderr)
            return "error"

    else:
        print("invalid action: " + action, file=sys.stderr)

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