#!/usr/bin/python3
import sys
import os
import o11
import json
import datetime
import pytz
import base64
from hashlib import sha256
from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH
from bs4 import BeautifulSoup
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

WVD_PATH = './WVD.wvd'

user = o11.parse_params(sys.argv, 'user')
password = o11.parse_params(sys.argv, 'password')

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

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

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

authFile = '/FibeTV_' + user + '.tokens'
user_agent = 'okhttp/4.12.0'

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-Encoding': 'gzip', 'User-Agent': user_agent}
    response = req.get(init_url, headers=headers, verify=False)
    psshs = to_pssh(response.content)
    return psshs[-1] if psshs else None

def do_cdm_external(pssh_data, device_id, ctoken, play_token):
    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 = {'X-Bell-UDID': device_id, 'X-Bell-Player-Agent': 'fonse-android/17.0.2.60602 Native/2.18.6.0035 (download;dynamicAdInsertion;livePause;widevine)', 'X-Bell-API-Key': 'fonse-android-9d760ba1', 'X-Bell-CToken': ctoken, 'Content-Type': 'application/octet-stream', 'X-Bell-Play-Token': play_token, 'User-Agent': user_agent}
    licence = req.post('https://ott-api.c.i.kprod.f0ns3.ca/api/license/v2/widevine/request', headers=lic_headers, data=challenge_data)
    cdm_obj.parse_license(session_id, licence.content)
    keys = [f"{key.kid.hex}:{key.key.hex()}" for key in cdm_obj.get_keys(session_id) if key.type != 'SIGNING']
    cdm_obj.close(session_id)
    return keys

def get_pssh_from_mpd(url):
    headers = {'Accept-Encoding': 'gzip', 'User-Agent': user_agent}
    response = req.get(url, headers=headers)
    soup = BeautifulSoup(response.content, features="xml")
    seg_template = soup.find('SegmentTemplate')
    if seg_template:
        init = seg_template.get('initialization', '')
        rep = soup.find('Representation')
        bandwidth = rep.get('bandwidth', '')
        rep_id = rep.get('id', '')
        base_url = soup.find('BaseURL')
        if base_url and 'http' in base_url.text:
            init_url = base_url.text + init.replace('$Bandwidth$', bandwidth).replace('$RepresentationID$', rep_id)
        else:
            init_url = url[:url.rfind('/') + 1] + init.replace('$Bandwidth$', bandwidth).replace('$RepresentationID$', rep_id)
        return init_to_pssh(init_url)
    return None

def get_channels(call_signs):
    headers = {'accept': 'application/json', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'x-bell-api-key': 'fonse-web-2d842ffc'}
    params = {'tvService': 'otto', 'epgChannelMap': 'MAP_TORONTO', 'epgVersion': '135006'}
    response = req.get('https://tv.bell.ca/api/epg/v3/channels', params=params, headers=headers)
    data = response.json()
    return sorted([d for d in data if d['callSign'] in call_signs], key=lambda k: k['number'])

def get_single(device_id, ctoken, channel_number, merge_id, account_id):
    headers = {'X-Bell-Player-Agent': 'fonse-android/17.0.2.60602 Native/2.18.6.0035 (download;dynamicAdInsertion;livePause;widevine)', 'Accept': 'application/json', 'X-Bell-UDID': device_id, 'X-Bell-API-Key': 'fonse-android-9d760ba1', 'X-Bell-CToken': ctoken, 'User-Agent': user_agent, 'Content-Type': 'application/json'}
    json_data = {'assetId': merge_id, 'type': 'LIVE', 'channelNumber': channel_number, 'mergedTvAccounts': [], 'outputTarget': 'DEVICE', 'programmingId': None, 'resolution': 'HD'}
    response = req.post(f'https://ott-api.c.i.kprod.f0ns3.ca/api/playback/v3/tvAccounts/{account_id}/streamings', headers=headers, json=json_data)
    data = response.json()
    policy = data['policies'][0]
    return policy['player']['streamingUrl'], policy['playToken']

def encrypt_with_aes_256_gcm(data, key):
    key_bytes = base64.b64decode(key)
    cipher = Cipher(algorithms.AES(key_bytes), modes.GCM(key_bytes), backend=default_backend())
    encryptor = cipher.encryptor()
    enc_data = encryptor.update(data.encode('utf-8')) + encryptor.finalize()
    return base64.b64encode(enc_data + encryptor.tag).decode('utf-8')

def get_salt(udid):
    headers = {'X-Bell-UDID': udid, 'X-Bell-API-Key': 'fonse-android-9d760ba1', 'Accept': 'application/json', 'User-Agent': user_agent}
    response = req.get('https://ott-api.c.i.kprod.f0ns3.ca/api/authnz/v3/salt', headers=headers)
    return response.json().get('salt', '')

def generate_cstoken(salt, username='null'):
    return sha256((salt + username + 'qfoVjuJ1Pb79GmRXh+7T3w==').encode('utf-8')).hexdigest()

def get_udid():
    headers = {'X-Bell-API-Key': 'fonse-android-9d760ba1', 'User-Agent': user_agent, 'Accept': '*/*', 'Content-Type': 'application/json'}
    json_data = {"previousUdid": "", "name": "o11device", "platform": "android", "model": "SM-G9650", "version": "9", "language": "en", "clientName": "fonse-android", "clientVersion": "17.0.2.60602"}
    response = req.post('https://ott-api.c.i.kprod.f0ns3.ca/api/device/v3/deviceEnrollments', headers=headers, json=json_data)
    data = response.json()
    return data['udid'], data['signature']

def do_login_request(username, pwd, udid, signature):
    salt = get_salt(udid)
    cs_token = generate_cstoken(salt, username)
    headers = {'X-Bell-UDID': udid, 'X-Bell-UDID-Signature': signature, 'Accept': 'application/json', 'X-Bell-API-Key': 'fonse-android-9d760ba1', 'User-Agent': user_agent, 'Content-Type': 'application/json'}
    json_data = {"accessNetwork": "WIFI", "username": username, "password": encrypt_with_aes_256_gcm(pwd, "qfoVjuJ1Pb79GmRXh+7T3w=="), "credentialsToken": None, "location": {"country": None, "latitude": 54.81545, "longitude": -106.54911}, "device": {"platform": "android", "model": "SM-G9650", "name": "SM-G9650", "version": "9", "language": "en"}, "client": {"name": "fonse-android", "version": "17.0.2.60602"}, "organization": "bell", "csToken": cs_token}
    response = req.post('https://ott-api.c.i.kprod.f0ns3.ca/api/authnz/v4/session', headers=headers, json=json_data)
    data = response.json()
    return data['ctoken'], data['credentialsToken'], data['tvAccounts'][0]

def refresh_login(udid, signature, ctoken, credentials_token, username):
    salt = get_salt(udid)
    cs_token = generate_cstoken(salt, username)
    headers = {'Accept': 'application/json', 'X-Bell-UDID': udid, 'X-Bell-UDID-Signature': signature, 'X-Bell-API-Key': 'fonse-android-9d760ba1', 'X-Bell-CToken': ctoken, 'User-Agent': user_agent, 'Content-Type': 'application/json'}
    json_data = {"accessNetwork": "WIFI", "username": username, "password": None, "credentialsToken": credentials_token, "location": {"country": None, "latitude": 58.51446, "longitude": -105.77308}, "device": {"platform": "android", "model": "SM-G9650", "name": "SM-G9650", "version": "9", "language": "en"}, "client": {"name": "fonse-android", "version": "17.0.2.60602"}, "organization": "bell", "csToken": cs_token}
    response = req.put('https://ott-api.c.i.kprod.f0ns3.ca/api/authnz/v4/session', headers=headers, json=json_data)
    data = response.json()
    return data['ctoken'], data['credentialsToken'], data['tvAccounts'][0]

def login():
    print("logging in...", file=sys.stderr)
    udid, signature = get_udid()
    ctoken, credentials_token, tv_account = do_login_request(user, password, udid, signature)
    account_id = str(tv_account['id'])
    call_signs = tv_account['epgSubscriptions']['callSigns']
    auth_data = {'username': user, 'ctoken': ctoken, 'credentials_token': credentials_token, 'udid': udid, 'signature': signature, 'account_id': account_id, 'call_signs': call_signs}
    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))
        ctoken, credentials_token, tv_account = refresh_login(auth['udid'], auth['signature'], auth['ctoken'], auth['credentials_token'], auth['username'])
        account_id = str(tv_account['id'])
        call_signs = tv_account['epgSubscriptions']['callSigns']
        device_id = auth['udid']
        auth['ctoken'] = ctoken
        auth['credentials_token'] = credentials_token
        auth['account_id'] = account_id
        auth['call_signs'] = call_signs
        json.dump(auth, open(os.path.abspath(os.path.dirname(__file__)) + authFile, 'w'))
    except:
        return "error"

    if action == "channels":
        output = {'Channels': []}
        for c in get_channels(call_signs):
            output['Channels'].append({'Name': c['name'], 'Mode': "live", 'SessionManifest': True, 'ManifestScript': 'number=' + str(c['number']) + '&mergeId=' + c['mergeId'], 'CdmType': "widevine", 'UseCdm': True, 'Cdm': 'number=' + str(c['number']) + '&mergeId=' + c['mergeId'], 'Video': 'best'})
        print(json.dumps(output, indent=2))
    elif action == "events":
        output = {'Events': []}
        for c in get_channels(call_signs):
            output['Events'].append({'Name': c['name'], 'Mode': "live", 'SessionManifest': True, 'ManifestScript': 'number=' + str(c['number']) + '&mergeId=' + c['mergeId'], 'CdmType': "widevine", 'UseCdm': True, 'Cdm': 'number=' + str(c['number']) + '&mergeId=' + c['mergeId'], 'Video': 'best', 'Autostart': True, 'Start': int(datetime.datetime.now(pytz.UTC).timestamp()), 'End': int((datetime.datetime.now(pytz.UTC) + datetime.timedelta(hours=4)).timestamp())})
        print(json.dumps(output, indent=2))
    elif action == "manifest":
        parts = id.split('&')
        channel_number = parts[0].replace('number=', '')
        merge_id = parts[1].replace('mergeId=', '') if len(parts) > 1 else ''
        url, play_token = get_single(device_id, ctoken, channel_number, merge_id, account_id)
        pssh_data = get_pssh_from_mpd(url)
        output = {"Cdn": [], "ManifestUrl": url, "Headers": {"Manifest": {'User-Agent': user_agent}, "Media": {'User-Agent': user_agent}}, "PlayToken": play_token}
        if pssh_data:
            output['Pssh'] = pssh_data
        print(json.dumps(output))
    elif action == "cdm" and cdm_param == "external":
        parts = id.split('&')
        channel_number = parts[0].replace('number=', '')
        merge_id = parts[1].replace('mergeId=', '') if len(parts) > 1 else ''
        url, play_token = get_single(device_id, ctoken, channel_number, merge_id, account_id)
        pssh_data = get_pssh_from_mpd(url)
        if pssh_data:
            for key in do_cdm_external(pssh_data, device_id, ctoken, play_token):
                print(key)

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