#!/usr/bin/python3
import sys
import os
import o11
import base64
import json
import datetime
import pytz
import requests
import m3u8
from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH

WVD_PATH = './WVD.wvd'

user = o11.parse_params(sys.argv, 'user')
password = o11.parse_params(sys.argv, 'password')
refresh_token_param = o11.parse_params(sys.argv, 'refreshToken')
device_param = 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')

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

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

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

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 do_cdm_external(pssh_data, 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.starplus.com',
        'referer': 'https://www.starplus.com/',
        'user-agent': user_agent,
        'x-application-version': '1.0.0',
        'x-bamsdk-client-id': 'star-22bcaf0a',
        'x-bamsdk-platform': 'javascript/windows/chrome',
        'x-bamsdk-version': '27.1',
    }

    licence = req.post('https://star.playback.edge.bamgrid.com/widevine/v1/obtain-license', 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_m3u8(url):
    try:
        obj = m3u8.load(url)
        for key in obj.session_keys:
            if key and key.keyformat == 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
                return key.uri.split(',')[-1]
    except:
        pass
    return None

def get_device_token():
    headers = {
        'accept': 'application/json',
        'authorization': 'c3RhciZicm93c2VyJjEuMC4w.COknIGCR7I6N0M5PGnlcdbESHGkNv7POwhFNL-_vIdg',
        'content-type': 'application/json',
        'origin': 'https://www.starplus.com',
        'referer': 'https://www.starplus.com/',
        'user-agent': user_agent,
        'x-application-version': '1.0.0',
        'x-bamsdk-client-id': 'star-22bcaf0a',
        'x-bamsdk-platform': 'javascript/windows/chrome',
        'x-bamsdk-platform-id': 'browser',
        'x-bamsdk-version': '27.1',
    }
    json_data = {
        'query': 'mutation registerDevice($input:RegisterDeviceInput!){registerDevice(registerDevice:$input){grant{grantType assertion}}}',
        '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': '122.0.0',
                    'brand': 'web',
                },
                'devicePlatformId': 'browser',
            },
        },
        'operationName': 'registerDevice',
    }
    response = req.post('https://star.api.edge.bamgrid.com/graph/v1/device/graphql', headers=headers, json=json_data)
    data = response.json()
    return data['extensions']['sdk']['token']['accessToken']

def do_login_request(username, pwd):
    device_token = get_device_token()
    headers = {
        'accept': 'application/json',
        'authorization': device_token,
        'content-type': 'application/json',
        'origin': 'https://www.starplus.com',
        'referer': 'https://www.starplus.com/',
        'user-agent': user_agent,
        'x-application-version': '1.0.0',
        'x-bamsdk-client-id': 'star-22bcaf0a',
        'x-bamsdk-platform': 'javascript/windows/chrome',
        'x-bamsdk-platform-id': 'browser',
        'x-bamsdk-version': '27.1',
    }
    json_data = {
        'query': 'mutation login($input: LoginInput!) { login(login: $input) { account { profiles { id } } } }',
        'variables': {'input': {'email': username, 'password': pwd}},
        'operationName': 'login',
    }
    response = req.post('https://star.api.edge.bamgrid.com/v1/public/graphql', headers=headers, json=json_data)
    data = response.json()
    token = data['extensions']['sdk']['token']['accessToken']
    profile_id = data['data']['login']['account']['profiles'][0]['id']
    return token, profile_id

def switch_profile(token, profile_id):
    headers = {
        'accept': 'application/json',
        'authorization': token,
        'content-type': 'application/json',
        'origin': 'https://www.starplus.com',
        'referer': 'https://www.starplus.com/',
        'user-agent': user_agent,
        'x-application-version': '1.0.0',
        'x-bamsdk-client-id': 'star-22bcaf0a',
        'x-bamsdk-platform': 'javascript/windows/chrome',
        'x-bamsdk-platform-id': 'browser',
        'x-bamsdk-version': '27.1',
    }
    json_data = {
        'query': 'mutation switchProfile($input: SwitchProfileInput!) { switchProfile(switchProfile: $input) { activeSession { sessionId } } }',
        'variables': {'input': {'profileId': profile_id}},
        'operationName': 'switchProfile',
    }
    response = req.post('https://star.api.edge.bamgrid.com/v1/public/graphql', headers=headers, json=json_data)
    data = response.json()
    return data['extensions']['sdk']['token']['accessToken'], data['extensions']['sdk']['token']['refreshToken']

def do_token_refresh(refresh_tok):
    headers = {
        'accept': 'application/json',
        'authorization': 'c3RhciZicm93c2VyJjEuMC4w.COknIGCR7I6N0M5PGnlcdbESHGkNv7POwhFNL-_vIdg',
        'content-type': 'application/json',
        'origin': 'https://www.starplus.com',
        'referer': 'https://www.starplus.com/',
        'user-agent': user_agent,
        'x-application-version': '1.0.0',
        'x-bamsdk-client-id': 'star-22bcaf0a',
        'x-bamsdk-platform': 'javascript/windows/chrome',
        'x-bamsdk-platform-id': 'browser',
        'x-bamsdk-version': '27.1',
    }
    json_data = {
        'query': 'mutation refreshToken($input:RefreshTokenInput!){refreshToken(refreshToken:$input){activeSession{sessionId}}}',
        'variables': {'input': {'refreshToken': refresh_tok}},
        'operationName': 'refreshToken',
    }
    response = req.post('https://star.api.edge.bamgrid.com/graph/v1/device/graphql', headers=headers, json=json_data)
    data = response.json()
    return data['extensions']['sdk']['token']['accessToken'], data['extensions']['sdk']['token']['refreshToken']

def get_channels(token):
    headers = {
        'accept': 'application/json',
        'authorization': 'Bearer ' + token,
        'origin': 'https://www.starplus.com',
        'referer': 'https://www.starplus.com/',
        'user-agent': user_agent,
        'x-application-version': '1.0.0',
        'x-bamsdk-client-id': 'star-22bcaf0a',
        'x-bamsdk-platform': 'javascript/windows/chrome',
        'x-bamsdk-version': '27.1',
    }
    response = req.get('https://star.content.edge.bamgrid.com/svc/content/CuratedSet/version/5.1/region/MX/audience/k-false,l-true/maturity/1850/language/es-419/setId/d074a95d-96d9-486b-9903-127608f92c85/pageSize/60/page/1', headers=headers)
    data = response.json()
    r = []
    for i in data['data']['CuratedSet']['items']:
        if i.get('eventState') == 'MID':
            r.append(i)
    # Get second set
    response2 = req.get('https://star.content.edge.bamgrid.com/svc/content/CuratedSet/version/5.1/region/MX/audience/k-false,l-true/maturity/1850/language/es-419/setId/a968c40f-b595-4679-b414-ed9519a3291a/pageSize/60/page/1', headers=headers)
    data2 = response2.json()
    for i in data2['data']['CuratedSet']['items']:
        if i.get('eventState') == 'MID':
            r.append(i)
    return r

def get_single(token, event_id, media_id):
    headers = {
        'accept': 'application/vnd.media-service+json; version=5',
        'authorization': token,
        'content-type': 'application/json',
        'origin': 'https://www.starplus.com',
        'referer': 'https://www.starplus.com/',
        'user-agent': user_agent,
        'x-application-version': '1.0.0',
        'x-bamsdk-client-id': 'star-22bcaf0a',
        'x-bamsdk-platform': 'javascript/windows/chrome',
        'x-bamsdk-version': '27.1',
    }
    json_data = {
        'playback': {
            'attributes': {
                'resolution': {'max': ['3840x2160']},
                'protocol': 'HTTPS',
                'assetInsertionStrategy': 'NONE',
                'playbackInitiationContext': 'ONLINE',
                'frameRates': [60],
            },
            'adTracking': {
                'limitAdTrackingEnabled': 'YES',
                'deviceAdId': '00000000-0000-0000-0000-000000000000',
            },
        },
    }
    response = req.post(f'https://star.playback.edge.bamgrid.com/events/{event_id}/media/{media_id}/scenarios/ctr-regular', headers=headers, json=json_data)
    data = response.json()
    return data['stream']['complete'][0]['url'].split('?')[0]

def login():
    print("logging in...", file=sys.stderr)
    token, profile_id = do_login_request(user, password)
    access_token, refresh_tok = switch_profile(token, profile_id)
    auth_data = {'accessToken': access_token, 'refreshToken': refresh_tok}
    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))
        access_token, refresh_tok = do_token_refresh(auth['refreshToken'])
        auth['accessToken'] = access_token
        auth['refreshToken'] = refresh_tok
        json.dump(auth, open(os.path.abspath(os.path.dirname(__file__)) + authFile, 'w'))
        token = access_token
    except:
        return "error"

    if action == "channels" or action == "events":
        output = {'Events': []} if action == "events" else {'Channels': []}
        channels = get_channels(token)
        for chan in channels:
            name = chan.get('internalTitle', '').split(' - ')[0]
            event_id = chan.get('eventId', '')
            media_id = chan.get('mediaMetadata', {}).get('mediaId', '')
            item = {
                'Name': name,
                'Mode': "live",
                'SessionManifest': True,
                'ManifestScript': f'eventId={event_id}&mediaId={media_id}',
                'CdmType': "widevine",
                'UseCdm': True,
                'Cdm': f'eventId={event_id}&mediaId={media_id}',
                '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":
        event_id = o11.parse_params(sys.argv, 'eventId')
        media_id = o11.parse_params(sys.argv, 'mediaId')
        if not event_id or not media_id:
            # Try parsing from id
            if '&' in id:
                parts = dict(p.split('=') for p in id.split('&'))
                event_id = parts.get('eventId', '')
                media_id = parts.get('mediaId', '')
        try:
            url = get_single(token, event_id, media_id)
            pssh_data = get_pssh_from_m3u8(url)
            
            output = {
                "Cdn": [],
                "ManifestUrl": url,
                "Headers": {
                    "Manifest": {'User-Agent': user_agent},
                    "Media": {'User-Agent': user_agent}
                },
                "Heartbeat": {
                    "Url": '',
                    "Params": '',
                    "PeriodMs": 5*60*1000
                },
                "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":
        try:
            lic_headers = {
                'authorization': 'Bearer ' + token,
                'content-type': 'application/octet-stream',
            }
            response = req.post('https://star.playback.edge.bamgrid.com/widevine/v1/obtain-license', 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":
        event_id = o11.parse_params(sys.argv, 'eventId')
        media_id = o11.parse_params(sys.argv, 'mediaId')
        if not event_id or not media_id:
            if '&' in id:
                parts = dict(p.split('=') for p in id.split('&'))
                event_id = parts.get('eventId', '')
                media_id = parts.get('mediaId', '')
        try:
            url = get_single(token, event_id, media_id)
            pssh_data = get_pssh_from_m3u8(url)
            
            if pssh_data:
                keys = do_cdm_external(pssh_data, 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()
