#!/usr/bin/python3
import sys
import os
import o11
import base64
import json
import datetime
import pytz
import requests
import secrets
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')
token_param = o11.parse_params(sys.argv, 'token')
device_param = o11.parse_params(sys.argv, 'device')

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 = '/A1XploreHR_' + 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, device_id):
    """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 = {
        'Origin': 'https://go.a1xploretv.hr',
        'Referer': 'https://go.a1xploretv.hr/',
        'User-Agent': user_agent,
        'Content-Type': 'application/x-www-form-urlencoded',
    }

    licence = req.post('https://vipottvmxdrmwv.vip.hr/?deviceId=' + device_id, 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 find_wv_pssh_offsets(raw):
    offsets = []
    offset = 0
    while True:
        offset = raw.find(b'pssh', offset)
        if offset == -1:
            break
        size = int.from_bytes(raw[offset-4:offset], byteorder='big')
        pssh_offset = offset - 4
        offsets.append(raw[pssh_offset:pssh_offset+size])
        offset += size
    return offsets

def to_pssh(content):
    wv_offsets = find_wv_pssh_offsets(content)
    return [base64.b64encode(wv_offset).decode() for wv_offset in wv_offsets]

def init_to_pssh(init_url):
    headers = {'accept': '*/*', 'user-agent': user_agent, 'content-type': 'application/octet-stream'}
    response = req.get(init_url, headers=headers)
    return to_pssh(response.content)

def get_pssh_from_mpd(url):
    headers = {'Accept': '*/*', 'User-Agent': user_agent}
    response = req.get(url, headers=headers)
    location = response.url
    soup = BeautifulSoup(response.content, features="xml")
    
    init = soup.find('SegmentTemplate')
    if init and init.get('initialization'):
        init_template = init['initialization']
        rep = soup.find('Representation')
        bandwidth = rep['bandwidth'] if rep else '0'
        rep_id = rep['id'] if rep else '0'
        
        loc_parts = location.split('/')
        loc_parts.pop()
        init_url = '/'.join(loc_parts) + '/' + init_template.replace('$Bandwidth$', bandwidth).replace('$RepresentationID$', rep_id)
        psshs = init_to_pssh(init_url)
        return psshs[-1] if psshs else None, location
    return None, location

def do_login_request(username, pwd, device_id=''):
    headers = {
        'User-Agent': user_agent, 'Content-Type': 'application/json', 'Accept': '*/*',
        'Origin': 'https://go.a1xploretv.hr', 'Referer': 'https://go.a1xploretv.hr/',
    }
    params = {'devId': device_id, 'user': username, 'pwd': pwd, 'rqT': 'true', 'refr': 'false'}
    if device_id:
        params['refr'] = 'true'
    response = req.post('https://web.a1xploretv.hr:8843/ext_dev_facade/auth/Login', headers=headers, params=params)
    return response.json()['token']

def check_token(token, device_id):
    headers = {'User-Agent': user_agent, 'Content-Type': 'application/json', 'Accept': '*/*', 'Origin': 'https://go.a1xploretv.hr', 'Referer': 'https://go.a1xploretv.hr/'}
    params = {'devId': device_id, 'token': token, 'apply': 'true'}
    response = req.get('https://web.a1xploretv.hr:8843/ext_dev_facade/auth/CheckToken', headers=headers, params=params)
    data = response.json()
    return data.get('status') == 'OK'

def get_device_id(token):
    cookies = {'webfw-1-auth': token}
    headers = {
        'Origin': 'https://go.a1xploretv.hr', 'Referer': 'https://go.a1xploretv.hr/',
        'SDSEVO_DEVICE_ID': 'none', 'SDSEVO_SESSION_ID': '[sedt=1]' + token,
        'User-Agent': user_agent, 'Zappware-User-Agent': 'windows_pc_chrome/v28.0.1 (Nexx 4.0 windows_pc_chrome; Windows; 10) null',
        'accept': '*/*', 'content-type': 'application/json',
    }
    json_data = {
        'operationName': 'limitedSetup', 'variables': {},
        'query': 'query limitedSetup { me { id firstName household { id devices { id items { id clientGeneratedId deviceType } } } } }'
    }
    response = req.post('https://web.a1xploretv.hr:8443/sdsmiddleware/Vipnet/graphql/4.0', cookies=cookies, headers=headers, json=json_data)
    data = response.json()
    devices = data['data']['me']['household']['devices']['items']
    for d in devices:
        if 'PC' in d.get('deviceType', ''):
            return d['id']
    return secrets.token_hex(16)

def get_bearer_cookies(token, device_id):
    cookies = {'webfw-1-auth': token}
    headers = {
        'Origin': 'https://go.a1xploretv.hr', 'Referer': 'https://go.a1xploretv.hr/',
        'SDSEVO_DEVICE_ID': device_id, 'SDSEVO_SESSION_ID': '[sedt=0]' + token,
        'User-Agent': user_agent, 'Zappware-User-Agent': 'windows_pc_chrome/v30.0.0 (Nexx 4.0 windows_pc_chrome; Windows; 10) null',
        'accept': '*/*', 'content-type': 'application/json',
    }
    json_data = {'operationName': 'stopPlayback', 'variables': {'input': {'sessionId': ''}}, 'query': 'mutation stopPlayback($input: StopPlaybackInput!) { stopPlayback(input: $input) { success } }'}
    response = req.post('https://web.a1xploretv.hr:8443/sdsmiddleware/Vipnet/graphql/4.0', headers=headers, json=json_data, cookies=cookies)
    return {**response.cookies.get_dict(), **cookies}

def get_profile_id(cookies):
    headers = {
        'Origin': 'https://go.a1xploretv.hr', 'Referer': 'https://go.a1xploretv.hr/',
        'User-Agent': user_agent, 'Zappware-User-Agent': 'windows_pc_chrome/v30.0.0 (Nexx 4.0 windows_pc_chrome; Windows; 10) null',
        'accept': '*/*', 'content-type': 'application/json',
    }
    json_data = {'operationName': 'getProfiles', 'variables': {}, 'query': 'query getProfiles { me { device { activeProfile { id } } } }'}
    response = req.post('https://web.a1xploretv.hr:8443/sdsmiddleware/Vipnet/graphql/4.0', cookies=cookies, headers=headers, json=json_data)
    return str(response.json()['data']['me']['device']['activeProfile']['id'])

def get_channels(cookies, profile_id):
    headers = {
        'Origin': 'https://go.a1xploretv.hr', 'Referer': 'https://go.a1xploretv.hr/',
        'User-Agent': user_agent, 'Zappware-User-Agent': 'windows_pc_chrome/v28.0.1 (Nexx 4.0 windows_pc_chrome; Windows; 10) null',
        'accept': '*/*', 'content-type': 'application/json',
    }
    current_time = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
    json_data = {
        'operationName': 'liveTV',
        'variables': {'profileId': profile_id, 'channelListId': '26-10534', 'channelAfterCursor': None, 'firstChannels': 300, 'currentTime': current_time, 'logoWidth': 76, 'logoHeight': 28, 'thumbnailHeight': 280, 'backgroundHeight': 780, 'backgroundWidth': 1920},
        'query': 'query liveTV($profileId: ID!, $channelAfterCursor: String, $firstChannels: Int!, $currentTime: Date!, $logoWidth: Int!, $logoHeight: Int!, $logoFlavour: ImageFlavour, $thumbnailHeight: Int!, $backgroundHeight: Int!, $backgroundWidth: Int!, $channelListId: ID!) { channelList(id: $channelListId) { channels(first: $firstChannels, after: $channelAfterCursor) { edges { node { id title } } } } }'
    }
    response = req.post('https://web.a1xploretv.hr:8443/sdsmiddleware/Vipnet/graphql/4.0', cookies=cookies, headers=headers, json=json_data)
    data = response.json()
    channels = []
    for e in data['data']['channelList']['channels']['edges']:
        channels.append(e['node'])
    return channels

def get_mpd(cookies, profile_id, channel_id):
    headers = {
        'Origin': 'https://go.a1xploretv.hr', 'Referer': 'https://go.a1xploretv.hr/',
        'User-Agent': user_agent, 'Zappware-User-Agent': 'windows_pc_chrome/v28.0.1 (Nexx 4.0 windows_pc_chrome; Windows; 10) null',
        'accept': '*/*', 'content-type': 'application/json',
    }
    json_data = {
        'operationName': 'playChannel',
        'variables': {'input': {'channelId': channel_id, 'replaceSessionId': None}, 'profileId': profile_id},
        'query': 'mutation playChannel($input: PlayChannelInput!, $profileId: ID!) { playChannel(input: $input) { playbackInfo { sessionId url } } }'
    }
    response = req.post('https://web.a1xploretv.hr:8443/sdsmiddleware/Vipnet/graphql/4.0', cookies=cookies, headers=headers, json=json_data)
    data = response.json()['data']
    playback_info = data['playChannel']['playbackInfo']
    return playback_info['url'], playback_info['sessionId']

def stop_playback(cookies):
    headers = {
        'Origin': 'https://go.a1xploretv.hr', 'Referer': 'https://go.a1xploretv.hr/',
        'User-Agent': user_agent, 'Zappware-User-Agent': 'windows_pc_chrome/v28.0.1 (Nexx 4.0 windows_pc_chrome; Windows; 10) null',
        'accept': '*/*', 'content-type': 'application/json',
    }
    json_data = {'operationName': 'stopPlayback', 'variables': {'input': {'sessionId': ''}}, 'query': 'mutation stopPlayback($input: StopPlaybackInput!) { stopPlayback(input: $input) { success } }'}
    req.post('https://web.a1xploretv.hr:8443/sdsmiddleware/Vipnet/graphql/4.0', cookies=cookies, headers=headers, json=json_data)

def login():
    print("logging in...", file=sys.stderr)
    token = do_login_request(user, password)
    device_id = get_device_id(token)
    token = do_login_request(user, password, device_id)
    auth_data = {'token': token, 'deviceId': device_id}
    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 = auth['token']
        device_id = auth['deviceId']
        if not check_token(token, device_id):
            return "error"
        cookies = get_bearer_cookies(token, device_id)
        profile_id = get_profile_id(cookies)
    except:
        return "error"

    if action == "channels":
        output = {'Channels': []}
        channels = get_channels(cookies, profile_id)
        for chan in channels:
            channel = {
                'Name': chan['title'],
                'Mode': "live",
                'SessionManifest': True,
                'ManifestScript': 'id=' + str(chan['id']),
                'CdmType': "widevine",
                'UseCdm': True,
                'Cdm': 'id=' + str(chan['id']),
                'Video': 'best',
                'OnDemand': True,
                'SpeedUp': True
            }
            output['Channels'].append(channel)
        print(json.dumps(output, indent=2))

    elif action == "events":
        output = {'Events': []}
        channels = get_channels(cookies, profile_id)
        for chan in channels:
            event = {
                'Name': chan['title'],
                'Mode': "live",
                'SessionManifest': True,
                'ManifestScript': 'id=' + str(chan['id']),
                'CdmType': "widevine",
                'UseCdm': True,
                'Cdm': 'id=' + str(chan['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())
            }
            output['Events'].append(event)
        print(json.dumps(output, indent=2))

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

    elif action == "manifest":
        channel_id = id
        try:
            url, session_id_val = get_mpd(cookies, profile_id, channel_id)
            stop_playback(cookies)
            pssh_data, final_url = get_pssh_from_mpd(url)
            
            output = {
                "Cdn": [],
                "ManifestUrl": final_url,
                "Headers": {"Manifest": {'User-Agent': user_agent}, "Media": {'User-Agent': user_agent}},
                "Heartbeat": {"Url": '', "Params": '', "PeriodMs": 5*60*1000},
                "DeviceId": device_id
            }
            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":
        dev_id = o11.parse_params(sys.argv, 'deviceId') or device_id
        try:
            lic_headers = {'Origin': 'https://go.a1xploretv.hr', 'Referer': 'https://go.a1xploretv.hr/', 'Content-Type': 'application/x-www-form-urlencoded'}
            response = req.post('https://vipottvmxdrmwv.vip.hr/?deviceId=' + dev_id, 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":
        channel_id = id
        try:
            url, session_id_val = get_mpd(cookies, profile_id, channel_id)
            stop_playback(cookies)
            pssh_data, final_url = get_pssh_from_mpd(url)
            
            if pssh_data:
                keys = do_cdm_external(pssh_data, device_id)
                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()
