#!/usr/bin/python3
import sys
import os
import base64
import json
import datetime
import pytz

# Add parent directory to path for o11 import
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import o11

from bs4 import BeautifulSoup
from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH

# Parse command line parameters
user = o11.parse_params(sys.argv, 'user')
password = o11.parse_params(sys.argv, 'password')
device = o11.parse_params(sys.argv, 'device')
pin = 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')

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

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

# Configuration
WVD_PATH = './WVD.wvd'
authFile = '/ESPN_tokens.json'
SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))

USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'
REGION = 'US'

headers = {
    'user-agent': USER_AGENT,
    'origin': 'https://www.espn.com',
    'referer': 'https://www.espn.com/',
}

def get_auth():
    try:
        return json.load(open(SCRIPT_DIR + authFile))
    except:
        return None

def save_auth(auth_data):
    json.dump(auth_data, open(SCRIPT_DIR + authFile, 'w'))

def extract_refresh(refresh_token):
    if '5=' in refresh_token:
        b64_token = refresh_token.replace('5=', '').split('|')[0]
        decoded = json.loads(base64.b64decode(b64_token).decode())
        return decoded['refresh_token']
    else:
        return refresh_token

def do_refresh(refresh_token):
    refresh_headers = {
        'User-Agent': USER_AGENT,
        'content-type': 'application/json',
        'Origin': 'https://www.espn.com',
        'Referer': 'https://www.espn.com/',
    }

    json_data = {
        'refreshToken': refresh_token
    }

    response = req.post('https://registerdisney.go.com/jgc/v8/client/ESPN-ONESITE.WEB-PROD/guest/refresh-auth?feature=no-password-reuse', headers=refresh_headers, json=json_data)
    
    data = response.json()
    return data['data']['token']['id_token'], data['data']['token']['refresh_token']

def login():
    print("logging in...", file=sys.stderr)
    
    auth = get_auth()
    if not auth or 'refresh_token' not in auth:
        print("No refresh token found. Please add refresh token to auth file.", file=sys.stderr)
        save_auth({'refresh_token': 'PASTE_ESPN-ONESITE.WEB-PROD.token_HERE'})
        sys.exit(1)
    
    try:
        refresh_token = extract_refresh(auth['refresh_token'])
        token, new_refresh = do_refresh(refresh_token)
        save_auth({'refresh_token': new_refresh, 'token': token})
        print("logged in successfully", file=sys.stderr)
        return token
    except Exception as e:
        print(f"Login failed: {e}", file=sys.stderr)
        sys.exit(1)

def get_token():
    auth = get_auth()
    if auth and 'token' in auth:
        return auth['token']
    return login()

def get_entitlement(token):
    ent_headers = {
        'accept': 'application/json',
        'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
        'referer': 'https://www.espn.com/',
        'user-agent': USER_AGENT,
    }

    data = {
        'user_token': token,
    }

    response = req.post('https://tve.dtci.technology/entitlements', headers=ent_headers, data=data)
    
    try:
        return response.json()['token']
    except:
        print('Failed getting entitlement', file=sys.stderr)
        return None

def get_anon_token():
    anon_headers = {
        'accept': 'application/json',
        'authorization': 'ZXNwbiZicm93c2VyJjEuMC4w.ptUt7QxsteaRruuPmGZFaJByOoqKvDP2a5YkInHrc7c',
        'content-type': 'application/json',
        'origin': 'https://plus.espn.com',
        'referer': 'https://plus.espn.com/',
        'user-agent': USER_AGENT,
        'x-application-version': '1.0',
        'x-bamsdk-client-id': 'espn-a9b93989',
        'x-bamsdk-platform': 'javascript/windows/chrome',
        'x-bamsdk-platform-id': 'browser',
        'x-bamsdk-version': '21.1',
    }

    json_data = {
        'query': 'mutation registerDevice($input: RegisterDeviceInput!) {\n            registerDevice(registerDevice: $input) {\n                grant {\n                    grantType\n                    assertion\n                }\n            }\n        }',
        'variables': {
            'input': {
                'deviceFamily': 'browser',
                'applicationRuntime': 'chrome',
                'deviceProfile': 'windows',
                'deviceLanguage': 'en-US',
                'attributes': {
                    'osDeviceIds': [],
                    'manufacturer': 'microsoft',
                    'model': None,
                    'operatingSystem': 'windows',
                    'operatingSystemVersion': '10.0',
                    'browserName': 'chrome',
                    'browserVersion': '123.0.0',
                    'brand': 'web',
                },
                'devicePlatformId': 'browser',
            },
        },
        'operationName': 'registerDevice',
    }

    response = req.post('https://espn.api.edge.bamgrid.com/graph/v1/device/graphql', headers=anon_headers, json=json_data)
    
    try:
        return response.json()['extensions']['sdk']['token']['accessToken']
    except:
        print('Failed getting anon token', file=sys.stderr)
        return None

def do_grant(auth_token, token):
    grant_headers = {
        'accept': 'application/json; charset=utf-8',
        'authorization': 'Bearer ' + auth_token,
        'content-type': 'application/json; charset=UTF-8',
        'origin': 'https://plus.espn.com',
        'referer': 'https://plus.espn.com/',
        'user-agent': USER_AGENT,
        'x-application-version': '1.0',
        'x-bamsdk-client-id': 'espn-a9b93989',
        'x-bamsdk-platform': 'javascript/windows/chrome',
        'x-bamsdk-version': '21.1',
    }

    json_data = {
        'id_token': token,
    }

    response = req.post('https://espn.api.edge.bamgrid.com/accounts/grant', headers=grant_headers, json=json_data)
    
    try:
        return response.json()['assertion']
    except:
        print('Failed getting grant', file=sys.stderr)
        return None

def get_play_token(assertion):
    play_headers = {
        'accept': 'application/json',
        'authorization': 'Bearer ZXNwbiZicm93c2VyJjEuMC4w.ptUt7QxsteaRruuPmGZFaJByOoqKvDP2a5YkInHrc7c',
        'content-type': 'application/x-www-form-urlencoded',
        'origin': 'https://plus.espn.com',
        'referer': 'https://plus.espn.com/',
        'user-agent': USER_AGENT,
        'x-application-version': '1.0',
        'x-bamsdk-client-id': 'espn-a9b93989',
        'x-bamsdk-platform': 'javascript/windows/chrome',
        'x-bamsdk-version': '21.1',
    }

    data = {
        'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
        'latitude': '0',
        'longitude': '0',
        'platform': 'browser',
        'subject_token': assertion,
        'subject_token_type': 'urn:bamtech:params:oauth:token-type:account',
    }

    response = req.post('https://espn.api.edge.bamgrid.com/token', headers=play_headers, data=data)
    
    try:
        return response.json()['access_token']
    except:
        print('Failed getting play token', file=sys.stderr)
        return None

def get_pssh_from_mpd(url):
    response = req.get(url, headers=headers)
    
    content_protections = BeautifulSoup(response.content, features="xml").findAll('ContentProtection')
    
    pssh_list = []
    for cp in content_protections:
        if cp.get('schemeIdUri', '').lower() == 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
            pssh_elem = cp.find('cenc:pssh')
            if pssh_elem:
                pssh_list.append(pssh_elem.text)
                
    return pssh_list

def do_cdm_internal(pssh_b64, lic_url):
    response = req.post(lic_url, headers={
        'origin': 'https://www.espn.com',
        'referer': 'https://www.espn.com/',
        'user-agent': USER_AGENT,
        'content-type': 'application/octet-stream',
    }, data=base64.b64decode(pssh_b64))
    
    response_b64 = str(base64.b64encode(response.content), 'ascii')
    if response_b64.startswith('CA'):
        return response_b64
    else:
        print(response.text, file=sys.stderr)
        return None

def do_cdm_external(pssh_b64, lic_url):
    """Extract Widevine keys using pywidevine library"""
    try:
        pssh_obj = PSSH(pssh_b64)
        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 = {
            'origin': 'https://www.espn.com',
            'referer': 'https://www.espn.com/',
            'user-agent': USER_AGENT,
            'content-type': 'application/octet-stream',
        }
        
        licence = req.post(lic_url, headers=lic_headers, data=challenge_data)
        
        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'CDM external failed: {e}', file=sys.stderr)
        return None

def do_action():
    token = get_token()
    
    if action == "login":
        login()
        sys.exit()
    
    if action == "channels":
        output = {}
        output['Channels'] = []
        
        params = {
            'apiKey': '0dbf88e8-cc6d-41da-aa83-18b5c630bc5c',
            'query': 'query Airings ( $countryCode: String!, $deviceType: DeviceType!, $tz: String!, $type: AiringType, $types: [AiringType], $categories: [String], $networks: [String], $packages: [String], $eventId: String, $packageId: String, $start: String, $end: String, $day: String, $limit: Int ) { airings( countryCode: $countryCode, deviceType: $deviceType, tz: $tz, type: $type, types: $types, categories: $categories, networks: $networks, packages: $packages, eventId: $eventId, packageId: $packageId, start: $start, end: $end, day: $day, limit: $limit ) { id airingId simulcastAiringId name shortName type startDateTime endDateTime shortDate: startDate(style: SHORT) authTypes adobeRSS duration feedName purchaseImage { url } image { url } network { id type abbreviation name shortName adobeResource isIpAuth } source { url authorizationType hasPassThroughAds hasNielsenWatermarks hasEspnId3Heartbeats commercialReplacement } packages { name } category { id name } subcategory { id name } sport { id name abbreviation code } league { id name abbreviation code } franchise { id name } program { id code categoryCode isStudio } tracking { nielsenCrossId1 nielsenCrossId2 comscoreC6 trackingId } } }',
            'variables': '{"deviceType":"DESKTOP","countryCode":"' + REGION + '","tz":"UTC+0200","type":"LIVE","packages":null,"limit":1000}'
        }
        
        response = req.get('https://watch.graph.api.espn.com/api', headers=headers, params=params)
        
        try:
            data = response.json()
            airings = data['data']['airings']
            
            for airing in airings:
                channel = {}
                name = airing.get('shortName', airing.get('name', 'Unknown'))
                if airing.get('feedName'):
                    name += f" ({airing['feedName']})"
                channel['Name'] = name
                channel['Mode'] = "live"
                channel['SessionManifest'] = True
                channel['ManifestScript'] = 'id=' + str(airing['id'])
                channel['CdmType'] = "widevine"
                channel['UseCdm'] = True
                channel['Cdm'] = 'id=' + str(airing['id'])
                channel['Video'] = 'best'
                channel['OnDemand'] = True
                channel['SpeedUp'] = True
                output['Channels'].append(channel)
            
            print(json.dumps(output, indent=2))
        except Exception as e:
            print(f"Error: {e}", file=sys.stderr)
            print(response.text, file=sys.stderr)
            return "error"
    
    elif action == "events":
        output = {}
        output['Events'] = []
        
        params = {
            'apiKey': '0dbf88e8-cc6d-41da-aa83-18b5c630bc5c',
            'query': 'query Airings ( $countryCode: String!, $deviceType: DeviceType!, $tz: String!, $type: AiringType, $types: [AiringType], $categories: [String], $networks: [String], $packages: [String], $eventId: String, $packageId: String, $start: String, $end: String, $day: String, $limit: Int ) { airings( countryCode: $countryCode, deviceType: $deviceType, tz: $tz, type: $type, types: $types, categories: $categories, networks: $networks, packages: $packages, eventId: $eventId, packageId: $packageId, start: $start, end: $end, day: $day, limit: $limit ) { id airingId simulcastAiringId name shortName type startDateTime endDateTime shortDate: startDate(style: SHORT) authTypes adobeRSS duration feedName purchaseImage { url } image { url } network { id type abbreviation name shortName adobeResource isIpAuth } source { url authorizationType hasPassThroughAds hasNielsenWatermarks hasEspnId3Heartbeats commercialReplacement } packages { name } category { id name } subcategory { id name } sport { id name abbreviation code } league { id name abbreviation code } franchise { id name } program { id code categoryCode isStudio } tracking { nielsenCrossId1 nielsenCrossId2 comscoreC6 trackingId } } }',
            'variables': '{"deviceType":"DESKTOP","countryCode":"' + REGION + '","tz":"UTC+0200","type":"UPCOMING","packages":null,"limit":1000}'
        }
        
        response = req.get('https://watch.graph.api.espn.com/api', headers=headers, params=params)
        
        try:
            data = response.json()
            airings = data['data']['airings']
            
            for airing in airings:
                event = {}
                name = airing.get('shortName', airing.get('name', 'Unknown'))
                if airing.get('feedName'):
                    name += f" ({airing['feedName']})"
                event['Name'] = name
                event['Mode'] = "live"
                event['SessionManifest'] = True
                event['ManifestScript'] = 'id=' + str(airing['id'])
                event['CdmType'] = "widevine"
                event['UseCdm'] = True
                event['Cdm'] = 'id=' + str(airing['id'])
                event['Video'] = 'best'
                event['Autostart'] = True
                
                # Parse start/end times
                if airing.get('startDateTime'):
                    date_object = datetime.datetime.strptime(airing['startDateTime'], "%Y-%m-%dT%H:%M:%SZ")
                    date_utc = date_object.replace(tzinfo=pytz.UTC)
                    event['Start'] = int(date_utc.timestamp())
                if airing.get('endDateTime'):
                    date_object = datetime.datetime.strptime(airing['endDateTime'], "%Y-%m-%dT%H:%M:%SZ")
                    date_utc = date_object.replace(tzinfo=pytz.UTC)
                    event['End'] = int(date_utc.timestamp())
                
                output['Events'].append(event)
            
            print(json.dumps(output, indent=2))
        except Exception as e:
            print(f"Error: {e}", file=sys.stderr)
            print(response.text, file=sys.stderr)
            return "error"
    
    elif action == "heartbeat":
        sys.exit()
    
    elif action == "manifest":
        try:
            # Get channel info
            params = {
                'apiKey': '0dbf88e8-cc6d-41da-aa83-18b5c630bc5c',
                'query': 'query Airings ( $countryCode: String!, $deviceType: DeviceType!, $tz: String!, $type: AiringType, $types: [AiringType], $categories: [String], $networks: [String], $packages: [String], $eventId: String, $packageId: String, $start: String, $end: String, $day: String, $limit: Int ) { airings( countryCode: $countryCode, deviceType: $deviceType, tz: $tz, type: $type, types: $types, categories: $categories, networks: $networks, packages: $packages, eventId: $eventId, packageId: $packageId, start: $start, end: $end, day: $day, limit: $limit ) { id airingId simulcastAiringId name shortName type startDateTime endDateTime shortDate: startDate(style: SHORT) authTypes adobeRSS duration feedName purchaseImage { url } image { url } network { id type abbreviation name shortName adobeResource isIpAuth } source { url authorizationType hasPassThroughAds hasNielsenWatermarks hasEspnId3Heartbeats commercialReplacement } packages { name } category { id name } subcategory { id name } sport { id name abbreviation code } league { id name abbreviation code } franchise { id name } program { id code categoryCode isStudio } tracking { nielsenCrossId1 nielsenCrossId2 comscoreC6 trackingId } } }',
                'variables': '{"deviceType":"DESKTOP","countryCode":"' + REGION + '","tz":"UTC+0200","type":"LIVE","packages":null,"limit":1000}'
            }
            
            response = req.get('https://watch.graph.api.espn.com/api', headers=headers, params=params)
            data = response.json()
            
            airing = None
            for a in data['data']['airings']:
                if str(a['id']) == id:
                    airing = a
                    break
            
            if not airing:
                print("Channel not found", file=sys.stderr)
                return "error"
            
            single_url = airing['source']['url']
            
            # Get stream URL
            if '{scenario}' in single_url:
                anon_token = get_anon_token()
                if anon_token:
                    assertion = do_grant(anon_token, token)
                    if assertion:
                        play_token = get_play_token(assertion)
                        if play_token:
                            play_headers = {
                                'accept': 'application/vnd.media-service+json; version=5',
                                'authorization': play_token,
                                'content-type': 'application/json',
                                'origin': 'https://plus.espn.com',
                                'referer': 'https://plus.espn.com/',
                                'user-agent': USER_AGENT,
                                'x-application-version': '1.0',
                                'x-bamsdk-client-id': 'espn-a9b93989',
                                'x-bamsdk-platform': 'javascript/windows/chrome',
                                'x-bamsdk-version': '21.1',
                            }
                            
                            json_data = {
                                'playback': {
                                    'attributes': {
                                        'resolution': {'max': ['4096x2160']},
                                        'protocol': 'HTTPS',
                                        'ads': 'adengine',
                                        'assetInsertionStrategy': 'ADPARTNER',
                                        'frameRates': [60],
                                    },
                                    'adTracking': {
                                        'limitAdTrackingEnabled': 'ERROR',
                                        'deviceAdId': '00000000-0000-0000-0000-000000000000',
                                    },
                                },
                            }
                            
                            resp = req.post(single_url.replace('{scenario}', 'silk-regular'), headers=play_headers, json=json_data)
                            stream_data = resp.json()
                            mpd_url = stream_data['stream']['slide'][0]['url']
            else:
                entitlement_token = get_entitlement(token)
                if entitlement_token:
                    asset_headers = {
                        'authorization': 'Bearer ' + entitlement_token,
                        'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
                        'origin': 'https://www.espn.com',
                        'referer': 'https://www.espn.com/',
                        'user-agent': USER_AGENT,
                    }
                    
                    asset_data = {
                        'authp': 'ziggo',
                        'devOS': 'Chrome',
                        'devType': 'desktop',
                        'isAutoplay': '1',
                        'isMute': '0',
                        'plt': 'desktop',
                        'drmSupport': 'DASH_WIDEVINE',
                    }
                    
                    resp = req.post(f'https://watch.auth.api.espn.com/video/auth/media/{id}/asset', params={'apikey': 'uiqlbgzdwuru14v627vdusswb'}, headers=asset_headers, data=asset_data)
                    stream_data = resp.json()
                    mpd_url = stream_data['stream']
            
            output = {
                "Cdn": [],
                "ManifestUrl": mpd_url,
                "Headers": {
                    "Manifest": {'User-Agent': USER_AGENT},
                    "Media": {'User-Agent': USER_AGENT}
                },
                "Heartbeat": {
                    "Url": '',
                    "Params": '',
                    "PeriodMs": 5*60*1000
                }
            }
            output['Cdn'].append({"Name": "default", "ManifestUrl": mpd_url})
            print(json.dumps(output))
        except Exception as e:
            print(f"Error: {e}", file=sys.stderr)
            return "error"
    
    elif action == "cdm" and cdm == "internal":
        try:
            # Get license URL
            lic_url = 'https://content.uplynk.com/wv'
            response = req.post(lic_url, headers={
                'origin': 'https://www.espn.com',
                'referer': 'https://www.espn.com/',
                'user-agent': USER_AGENT,
                'content-type': 'application/octet-stream',
            }, 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":
        try:
            lic_url = 'https://content.uplynk.com/wv'
            keys = do_cdm_external(pssh, lic_url)
            if keys:
                for key in keys:
                    print(key)
            else:
                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()
