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

# 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 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 = '/Hulu_auth.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/131.0.0.0 Safari/537.36'

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

# Store license URL for CDM operations
lic_url_cache = {}

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'), indent=2)

def current_milli_time():
    return round(time.time() * 1000)

def get_session(hulu_dt, hulu_pid, hulu_pgid):
    cookies = {
        '_hulu_dt': hulu_dt,
        'utag_main__ss': '0%3Bexp-session',
        '_hulu_pgid': hulu_pgid,
        'utag_main__st': f'{current_milli_time()}%3Bexp-session',
        'utag_main__prevpage': f'/hub/home%3Bexp-{current_milli_time()}',
        'utag_main_dc_event': '23%3Bexp-session',
        '_hulu_pid': hulu_pid,
        '_hulu_pprotect': str(current_milli_time()),
    }

    session_headers = {
        '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',
        'referer': 'https://www.hulu.com/onboarding',
        'user-agent': USER_AGENT,
    }

    response = req.get('https://www.hulu.com/hub/home', cookies=cookies, headers=session_headers)
    
    r_cookies = response.cookies if hasattr(response, 'cookies') else {}
    if hasattr(r_cookies, 'get_dict'):
        r_cookies = r_cookies.get_dict()
    
    return r_cookies.get('_hulu_session', ''), r_cookies.get('_hulu_pgid', hulu_pgid)

def login():
    print("logging in...", file=sys.stderr)
    
    auth = get_auth()
    if not auth or 'hulu_dt' not in auth:
        print("No auth found. Please add Hulu cookies to auth file.", file=sys.stderr)
        save_auth({
            'hulu_dt': '_hulu_dt_HERE',
            'hulu_pid': '_hulu_pid_HERE',
            'hulu_pgid': '_hulu_pgid_HERE'
        })
        sys.exit(1)
    
    try:
        hulu_session, hulu_pgid = get_session(auth['hulu_dt'], auth['hulu_pid'], auth['hulu_pgid'])
        auth['hulu_session'] = hulu_session
        auth['hulu_pgid'] = hulu_pgid
        save_auth(auth)
        print("logged in successfully", file=sys.stderr)
        return hulu_session
    except Exception as e:
        print(f"Login failed: {e}", file=sys.stderr)
        sys.exit(1)

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

def get_listing(channel_id, token):
    cookies = {'_hulu_session': token}
    
    listing_headers = {
        'accept': '*/*',
        'content-type': 'application/json',
        'origin': 'https://www.hulu.com',
        'referer': 'https://www.hulu.com/',
        'user-agent': USER_AGENT,
    }

    params = {
        'lat': '41.0895249',
        'long': '-73.8419063',
    }

    json_data = {
        'channels': [channel_id],
    }

    response = req.post('https://guide.hulu.com/guide/listing', params=params, cookies=cookies, headers=listing_headers, json=json_data)
    
    try:
        data = response.json()
        return data['channels'][0]['programs'][0]['eab']
    except:
        print('Failed getting listing', file=sys.stderr)
        return None

def get_single(channel_id, content_eab_id, token):
    cookies = {'_hulu_session': token}
    
    single_headers = {
        'accept': '*/*',
        'content-type': 'application/json',
        'origin': 'https://www.hulu.com',
        'referer': 'https://www.hulu.com/',
        'user-agent': USER_AGENT,
        'x-hulu-user-agent': 'site-player/310358',
    }

    json_data = {
        'deejay_device_id': 214,
        'version': 1,
        'all_cdn': True,
        'content_eab_id': content_eab_id,
        'region': 'US',
        'xlink_support': False,
        'limit_ad_tracking': False,
        'privacy_opt_out': 'NO',
        'ignore_kids_block': False,
        'language': 'en',
        'unencrypted': True,
        'include_t2_revenue_beacon': '1',
        'include_t2_rev_beacon': True,
        'include_t2_adinteraction_beacon': True,
        'interface_version': '1.15.1',
        'network_mode': 'wifi',
        'is_html5_player': True,
        'support_js': True,
        'support_vpaid': True,
        'channel_id': channel_id,
        'play_intent': 'live',
        'playback': {
            'version': 2,
            'video': {
                'codecs': {
                    'values': [{'type': 'H264', 'width': 1920, 'height': 1080, 'framerate': 60, 'level': '4.2', 'profile': 'HIGH'}],
                    'selection_mode': 'ONE',
                },
            },
            'audio': {
                'codecs': {
                    'values': [{'type': 'AAC'}],
                    'selection_mode': 'ONE',
                },
            },
            'drm': {
                'values': [
                    {'type': 'WIDEVINE', 'version': 'MODULAR', 'security_level': 'L3'},
                    {'type': 'PLAYREADY', 'version': 'V2', 'security_level': 'SL2000'},
                ],
                'selection_mode': 'ALL',
            },
            'manifest': {
                'type': 'HLS',
                'https': True,
                'multiple_cdns': True,
                'patch_updates': True,
                'hulu_types': True,
                'live_dai': True,
                'multiple_periods': True,
                'xlink': False,
                'secondary_audio': True,
                'unified_asset_signaling': False,
                'live_fragment_delay': 3,
            },
            'segments': {
                'values': [{'type': 'FMP4', 'encryption': {'mode': 'CENC', 'type': 'CENC'}, 'https': True}],
                'selection_mode': 'ONE',
            },
        },
    }

    response = req.post('https://play.hulu.com/v6/playlist', cookies=cookies, headers=single_headers, json=json_data)
    
    try:
        data = response.json()
        return data['stream_url'], data.get('wv_server', '')
    except:
        print('Failed getting single', file=sys.stderr)
        return None, None

def get_pssh_from_hls(url):
    try:
        response = req.get(url, headers=headers)
        content = response.text if hasattr(response, 'text') else response.content.decode()
        return content.split('#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;base64,')[-1].split('"')[0]
    except:
        return None

def do_cdm_internal(challenge_b64, lic_url):
    lic_headers = {
        'accept': '*/*',
        'origin': 'https://www.hulu.com',
        'referer': 'https://www.hulu.com/',
        'user-agent': USER_AGENT,
        'content-type': 'application/octet-stream',
    }
    
    response = req.post(lic_url, headers=lic_headers, data=base64.b64decode(challenge_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 = {
            'accept': '*/*',
            'origin': 'https://www.hulu.com',
            'referer': 'https://www.hulu.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'] = []
        
        cookies = {'_hulu_session': token}
        
        params = {
            'lat': '41.0895249',
            'long': '-73.8419063',
            'cacheKey': '0.61408736778431',
        }

        response = req.get('https://guide.hulu.com/guide/views', params=params, cookies=cookies, headers=headers)
        
        try:
            data = response.json()
            channels = data['views'][2]['details']['channels']
            
            for ch in channels:
                channel = {}
                channel['Name'] = ch['name']
                channel['Mode'] = "live"
                channel['SessionManifest'] = True
                channel['ManifestScript'] = 'id=' + str(ch['id'])
                channel['CdmType'] = "widevine"
                channel['UseCdm'] = True
                channel['Cdm'] = 'id=' + str(ch['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'] = []
        # Hulu Live TV doesn't have scheduled events in the same way
        # Return empty events list
        print(json.dumps(output, indent=2))
    
    elif action == "heartbeat":
        sys.exit()
    
    elif action == "manifest":
        try:
            content_eab_id = get_listing(id, token)
            if not content_eab_id:
                return "error"
            
            stream_url, wv_server = get_single(id, content_eab_id, token)
            if not stream_url:
                return "error"
            
            # Cache license URL for CDM operations
            lic_url_cache[id] = wv_server
            
            output = {
                "Cdn": [],
                "ManifestUrl": stream_url,
                "Headers": {
                    "Manifest": {'User-Agent': USER_AGENT},
                    "Media": {'User-Agent': USER_AGENT}
                },
                "Heartbeat": {
                    "Url": '',
                    "Params": '',
                    "PeriodMs": 5*60*1000
                },
                "LicenseUrl": wv_server
            }
            output['Cdn'].append({"Name": "default", "ManifestUrl": stream_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 - need to fetch stream info first
            content_eab_id = get_listing(id, token)
            if content_eab_id:
                _, wv_server = get_single(id, content_eab_id, token)
                if wv_server:
                    result = do_cdm_internal(challenge, wv_server)
                    if result:
                        print(result)
                    else:
                        return "error"
                else:
                    return "error"
            else:
                return "error"
        except Exception as e:
            print(f"Error: {e}", file=sys.stderr)
            return "error"
    
    elif action == "cdm" and cdm == "external":
        try:
            # Get license URL
            content_eab_id = get_listing(id, token)
            if content_eab_id:
                stream_url, wv_server = get_single(id, content_eab_id, token)
                if wv_server:
                    # Get PSSH from stream if not provided
                    pssh_to_use = pssh
                    if not pssh_to_use and stream_url:
                        pssh_to_use = get_pssh_from_hls(stream_url)
                    
                    if pssh_to_use:
                        keys = do_cdm_external(pssh_to_use, wv_server)
                        if keys:
                            for key in keys:
                                print(key)
                        else:
                            return "error"
                    else:
                        print("No PSSH available", file=sys.stderr)
                        return "error"
                else:
                    return "error"
            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()
