dut-iptv icon indicating copy to clipboard operation
dut-iptv copied to clipboard

[Not working anymore] Videoland - Sign In Fix

Open Nigel1992 opened this issue 3 years ago • 5 comments

Not working anymore See https://github.com/dut-iptv/dut-iptv/issues/144#issuecomment-1352449715


I was unable to sign-in to Videoland. It apparently has something to do with the cookies. To temp fix it, just download the following fixed version of Videoland: plugin.video.videoland.zip

If you are not familiar with Python, dont bother reading the below info.

So I fixed it by adding the gmid + ucid + hasGmid cookies :) Working example of api.py below [using static/contant cookies] [might work for others?]

import base64, json, os, random, re, string, time, xbmc

from collections import OrderedDict
from resources.lib.base.l1.constants import ADDON_ID, ADDON_PROFILE
from resources.lib.base.l2 import settings
from resources.lib.base.l2.log import log
from resources.lib.base.l3.language import _
from resources.lib.base.l3.util import check_key, get_credentials, encode32, is_file_older_than_x_days, is_file_older_than_x_minutes, load_file, load_profile, load_prefs, remove_dir, remove_file, save_profile, save_prefs, set_credentials, write_file
from resources.lib.base.l4.exceptions import Error
from resources.lib.base.l4.
session import Session
from resources.lib.base.l5.api import api_download, api_get_channels
from resources.lib.constants import CONST_BASE_HEADERS, CONST_URLS, CONST_IMAGES
from urllib.parse import parse_qs, urlparse, quote_plus

#Included from base.l7.plugin
#api_clean_after_playback
#api_get_info

#Included from base.l8.menu
#api_add_to_watchlist
#api_get_profiles
#api_list_watchlist
#api_login
#api_play_url
#api_remove_from_watchlist
#api_search
#api_set_profile
#api_vod_download
#api_vod_season
#api_vod_seasons
#api_watchlist_listing

def api_add_to_watchlist(id, series='', season='', program_type='', type='watchlist'):
    if not api_get_session():
        return None

    if series and len(str(series)) > 0:
        id = series

    if type == 'watchlist':
        watchlist_url = '{base_url}/api/v3/watchlist/{id}'.format(base_url=CONST_URLS['base'], id=id)
    else:
        return False

    headers = api_get_headers(personal=False)

    download = api_download(url=watchlist_url, type='post', headers=headers, data=None, json_data=False, return_json=False)
    code = download['code']

    if not code or not code == 200:
        return False

    return True

def api_clean_after_playback(stoptime):
    profile_settings = load_profile(profile_id=1)

    if len(str(profile_settings['ticket_id'])) > 0:
        offset = "00:00:00"

        if stoptime > 0:
            m, s = divmod(stoptime, 60)
            h, m = divmod(m, 60)
            offset = '{:02d}:{:02d}:{:02d}'.format(h, m, s)

        session_post_data = {
            'action': "stop",
            'buffer_state': 0,
            'buffer_total': 1,
            'offset': offset,
            'token': profile_settings['token']
        }

        headers = api_get_headers(personal=False)

        stop_url = '{base_url}/api/v3/heartbeat/{ticket_id}?action=stop&offset={offset}'.format(base_url=CONST_URLS['base'], ticket_id=profile_settings['ticket_id'], offset=offset)

        download = api_download(url=stop_url, type='post', headers=headers, data=session_post_data, json_data=True, return_json=True)

        session_post_data['action'] = 'exit'

        exit_url = '{base_url}/api/v3/heartbeat/{ticket_id}?action=exit&offset={offset}'.format(base_url=CONST_URLS['base'], ticket_id=profile_settings['ticket_id'], offset=offset)

        download = api_download(url=exit_url, type='post', headers=headers, data=session_post_data, json_data=True, return_json=True)

def api_get_headers(personal=False):
    headers = { 'videoland-platform': 'videoland' }

    if personal == True:
        pass

    return headers

def api_get_info(id, channel=''):
    #profile_settings = load_profile(profile_id=1)

    info = {}

    return info

def api_get_session(force=0, return_data=False):
    force = int(force)

    profile_url = '{base_url}/api/v3/profiles'.format(base_url=CONST_URLS['base'])

    headers = api_get_headers(personal=False)
    headers["Referer"] = "{base_url}/profielkeuze".format(base_url=CONST_URLS['base'])

    download = api_download(url=profile_url, type='get', headers=headers, data=None, json_data=False, return_json=True)
    data = download['data']
    code = download['code']

    if not code or not code == 200 or not data or not check_key(data[0], 'id'):
        login_result = api_login()

        if not login_result['result']:
            if return_data == True:
                return {'result': False, 'data': login_result['data'], 'code': login_result['code']}

            return False
        else:
            download = api_download(url=profile_url, type='get', headers=headers, data=None, json_data=False, return_json=True)
            data = download['data']
            code = download['code']

    profile_settings = load_profile(profile_id=1)
    profile_settings['last_login_success'] = 1
    profile_settings['last_login_time'] = int(time.time())
    save_profile(profile_id=1, profile=profile_settings)

    if return_data == True:
        return {'result': True, 'data': data, 'code': code}

    return True

def api_get_profiles():
    profiles = api_get_session(force=0, return_data=True)
    return_profiles = {}

    if profiles['result'] == True:
        for result in profiles['data']:
            if result['is_active'] == True:
                return_profiles[result['id']] = {}
                return_profiles[result['id']]['id'] = result['id']
                return_profiles[result['id']]['name'] = result['profilename']

    return return_profiles

def api_list_watchlist(type='watchlist'):
    if not api_get_session():
        return None

    profile_settings = load_profile(profile_id=1)

    headers = api_get_headers(personal=False)

    if type == 'continuewatch':
        watchlist_url = '{base_url}/api/v3/progress/{id}'.format(base_url=CONST_URLS['base'], id=profile_settings['profile_id'])
    elif type == 'watchlist':
        watchlist_url = '{base_url}/api/v3/watchlist/{id}'.format(base_url=CONST_URLS['base'], id=profile_settings['profile_id'])

    download = api_download(url=watchlist_url, type='get', headers=headers, data=None, json_data=False, return_json=True)
    data = download['data']
    code = download['code']

    if code and code == 200 and data and check_key(data, 'count'):
        return data

    return None

def api_login():
    creds = get_credentials()
    username = creds['username']
    password = creds['password']

    remove_file(file='stream_cookies', ext=False)

    profile_settings = load_profile(profile_id=1)
    profile_settings['vlid'] = ''
    profile_settings['profile_id'] = ''

    save_profile(profile_id=1, profile=profile_settings)

    headers = {
        "Origin": CONST_URLS['gigya'],
        "Referer": "{gigya_url}/".format(gigya_url=CONST_URLS['gigya']),
	"Cookie": "gmid=gmid.ver4.AcbH8QbssA.v4cJjgoAsSCf9Zdj_avWgcJ9HV2e6AWBNGkcWUjHS0xdUzsDk76vyzDMvFQ23hNh.bUx-tQpqtFBZFdJpjLC6K7xRVnLBDdtOhQEDNyidI_qDKlktYOciFkL_3T3OWYqaHNtNvQXbBcZ9bZppDcXd3g.sc3; ucid=pwJ04qPJUZvOT0Y2bmqTmQ; hasGmid=ver4"
}

    session_post_data = {
        'loginID': username,
        'password': password,
        'sessionExpiration': '0',
        'targetEnv': 'jssdk',
        'include': 'profile,data',
        'includeUserInfo': 'true',
        'lang': 'en',
        'APIKey': '3_t2Z1dFrbWR-IjcC-Bod1kei6W91UKmeiu3dETVG5iKaY4ILBRzVsmgRHWWo0fqqd',
        'sdk': 'js_latest',
        'authMode': 'cookie',
        'pageURL': "{base_url}/".format(base_url=CONST_URLS['base']),
        'format': 'json',
    }

    login_url = '{gigya_url}/accounts.login'.format(gigya_url=CONST_URLS['gigya'])

    download = api_download(url=login_url, type='post', headers=headers, data=session_post_data, json_data=False, return_json=True)
    data = download['data']
    code = download['code']

    if not code or not code == 200 or not data or not check_key(data, 'signatureTimestamp') or not check_key(data, 'UID') or not check_key(data, 'UIDSignature'):
        return { 'code': code, 'data': data, 'result': False }

    hash_url = '{base_url}/api/v3/login/hash'.format(base_url=CONST_URLS['base'])

    headers = api_get_headers(personal=False)
    headers["Referer"] = "{base_url}/inloggen".format(base_url=CONST_URLS['base'])
    headers["Origin"] = CONST_URLS['base']

    session_post_data = {
        'UID': data['UID'],
        'UIDSignature': data['UIDSignature'],
        'signatureTimestamp': data['signatureTimestamp'],
    }

    download = api_download(url=hash_url, type='post', headers=headers, data=session_post_data, json_data=True, return_json=True)
    data = download['data']
    code = download['code']

    if not code or not code == 200 or not data or not check_key(data, 'vlHash'):
        return { 'code': code, 'data': data, 'result': False }

    profile_settings['vlid'] = data['vlHash']

    cookies = load_file(file='stream_cookies', isJSON=True)

    if not cookies:
        cookies = {}

    cookies['vlId'] = data['vlHash']

    write_file(file='stream_cookies', data=cookies, isJSON=True)

    profile_url = '{base_url}/api/v3/profiles'.format(base_url=CONST_URLS['base'])

    headers = api_get_headers(personal=False)
    headers["Referer"] = "{base_url}/profielkeuze".format(base_url=CONST_URLS['base'])

    download = api_download(url=profile_url, type='get', headers=headers, data=None, json_data=False, return_json=True)
    data = download['data']
    code = download['code']

    if not code or not code == 200 or not data or not check_key(data[0], 'id'):
        return { 'code': code, 'data': data, 'result': False }

    save_profile(profile_id=1, profile=profile_settings)

    api_set_profile()

    return { 'code': code, 'data': data, 'result': True }

def api_play_url(type, channel=None, id=None, video_data=None, from_beginning=0, pvr=0, change_audio=0):
    playdata = {'path': '', 'license': '', 'info': '', 'properties': {}}

    if not api_get_session():
        return playdata

    from_beginning = int(from_beginning)
    pvr = int(pvr)
    change_audio = int(change_audio)

    profile_settings = load_profile(profile_id=1)

    info = {}
    properties = {}

    if not type or not len(str(type)) > 0:
        return playdata

    headers = api_get_headers(personal=False)

    if type == 'channel':
        play_url_path = id

        download = api_download(url=play_url_path, type='get', headers=headers, data=None, json_data=False, return_json=True)
        data = download['data']
        code = download['code']

        if not code or not code == 200 or not data or not check_key(data, 'manifest') or not check_key(data, 'token'):
            return playdata

        path = data['manifest']

        if check_key(data, 'licenseUrl'):
            license = data['licenseUrl']

        profile_settings['ticket_id'] = ''
        profile_settings['token'] = data['token']
        save_profile(profile_id=1, profile=profile_settings)

        mpd = ''

        playdata = {'path': path, 'mpd': mpd, 'license': license, 'info': info, 'properties': properties}

        return playdata

    if id.startswith('E'):
        typestr = 'episode'
        id = id[1:]
        id_ar = id.split('###')
        series = id_ar[0]
        season = id_ar[1]
        id = id_ar[2]
        info_url = '{base_url}/api/v3/episodes/{series}/{season}'.format(base_url=CONST_URLS['base'], series=series, season=season)
    elif id.startswith('M'):
        typestr = 'movie'
        id = id[1:]
        info_url = '{base_url}/api/v3/movies/{id}'.format(base_url=CONST_URLS['base'], id=id)
    else:
        return playdata

    download = api_download(url=info_url, type='get', headers=headers, data=None, json_data=False, return_json=True)
    data = download['data']
    code = download['code']

    if code and code == 200 and data:
        if typestr == 'episode':
            if check_key(data, 'details') and check_key(data['details'], 'E' + str(id)):
                info = data['details']['E' + str(id)]
        else:
            info = data

    play_url_path = '{base_url}/api/v3/stream/{id}/widevine?edition=&profile_id={profile_id}'.format(base_url=CONST_URLS['base'], id=id, profile_id=profile_settings['profile_id'])

    download = api_download(url=play_url_path, type='get', headers=headers, data=None, json_data=False, return_json=True)
    data = download['data']
    code = download['code']

    if not code or not code == 200 or not data or not check_key(data, 'id') or not check_key(data, 'ticket_id') or not check_key(data, 'stream') or not check_key(data['stream'], 'dash'):
        return playdata

    path = data['stream']['dash']

    if check_key(data, 'drm'):
        license = data['drm']

    profile_settings['ticket_id'] = data['ticket_id']
    profile_settings['token'] = data['stream']['token']
    save_profile(profile_id=1, profile=profile_settings)

    mpd = ''

    if change_audio == 1:
        download = api_download(url=path, type='get', headers=headers, data=None, json_data=False, return_json=False)
        data = download['data']
        code = download['code']

        if code and code == 200:
            mpd = data

    playdata = {'path': path, 'mpd': mpd, 'license': license, 'info': info, 'properties': properties}

    return playdata

def api_remove_from_watchlist(id, type='watchlist'):
    if not api_get_session():
        return None

    headers = api_get_headers(personal=False)

    if type == 'continuewatch':
        remove_url = '{base_url}/api/v3/progress/{id}'.format(base_url=CONST_URLS['base'], id=id)
    elif type == 'watchlist':
        remove_url = '{base_url}/api/v3/watchlist/{id}'.format(base_url=CONST_URLS['base'], id=id)

    download = api_download(url=remove_url, type='delete', headers=headers, data=None, json_data=False, return_json=False)
    code = download['code']

    if not code or (not code == 200 and not code == 204):
        return False

    return True

def api_search(query):
    return None

def api_set_profile(id=''):
    profile_settings = load_profile(profile_id=1)
    profiles = api_get_session(force=0, return_data=True)

    name = ''
    owner_id = ''
    owner_name = ''
    saved_id = ''
    saved_name = ''

    if not profiles or profiles['result'] == False:
        return False

    for result in profiles['data']:
        if result['is_account_owner'] == True:
            owner_id = result['id']
            owner_name = result['profilename']

        if result['is_active'] == True and result['id'] == id:
            name = result['profilename']

        if check_key(profile_settings, 'profile_id'):
            if result['is_active'] == True and result['id'] == profile_settings['profile_id']:
                saved_id = result['id']
                saved_name = result['profilename']

    if len(str(name)) == 0:
        if len(str(saved_name)) > 0:
            id = saved_id
            name = saved_name
        else:
            id = owner_id
            name = owner_name

    switch_url = '{base_url}/api/v3/profiles/{id}/switch'.format(base_url=CONST_URLS['base'], id=id)

    headers = api_get_headers(personal=False)
    headers["Referer"] = "{base_url}/profielkeuze".format(base_url=CONST_URLS['base'])

    session_post_data = {
        'customer_id': id
    }

    download = api_download(url=switch_url, type='post', headers=headers, data=session_post_data, json_data=False, return_json=False)
    code = download['code']

    if not code or not code == 204:
        return False

    profile_settings = load_profile(profile_id=1)
    profile_settings['profile_name'] = name
    profile_settings['profile_id'] = id
    save_profile(profile_id=1, profile=profile_settings)

    return True

def api_vod_download():
    return None

def api_vod_season(series, id, use_cache=True):
    type = "vod_season_{id}".format(id=id)
    type = encode32(type)

    file = os.path.join("cache", "{type}.json".format(type=type))

    id_ar = id.split('###')
    series = id_ar[0]
    seasonstr = id_ar[1]
    cache = 0

    if not is_file_older_than_x_days(file=os.path.join(ADDON_PROFILE, file), days=0.5) and use_cache == True:
        data = load_file(file=file, isJSON=True)
        cache = 1
    else:
        headers = api_get_headers(personal=False)

        seasons_url = '{base_url}/api/v3/series/{series}/seasons/{season}'.format(base_url=CONST_URLS['base'], series=series, season=seasonstr)

        download = api_download(url=seasons_url, type='get', headers=headers, data=None, json_data=False, return_json=True)
        data = download['data']
        code = download['code']

        if code and code == 200 and data and check_key(data, 'title'):
            write_file(file=file, data=data, isJSON=True)

    return {'data': data, 'cache': cache}

def api_vod_seasons(type, id, use_cache=True):
    type = "vod_seasons_{id}".format(id=id)
    type = encode32(type)

    file = os.path.join("cache", "{type}.json".format(type=type))

    id = id[1:]
    cache = 0

    if not is_file_older_than_x_days(file=os.path.join(ADDON_PROFILE, file), days=0.5) and use_cache == True:
        data = load_file(file=file, isJSON=True)
        cache = 1
    else:
        headers = api_get_headers(personal=False)

        seasons_url = '{base_url}/api/v3/series/{series}'.format(base_url=CONST_URLS['base'], series=id)

        download = api_download(url=seasons_url, type='get', headers=headers, data=None, json_data=False, return_json=True)
        data = download['data']
        code = download['code']

        if code and code == 200 and data and check_key(data, 'title'):
            write_file(file=file, data=data, isJSON=True)

    return {'data': data, 'cache': cache}

def api_vod_subscription():
    return None

def api_watchlist_listing():
    return None

NOTE: I am not a programmer, but I am familiar with HTTP. To "hopefully" fix this permanently, you'll need to parse the cookies.

These cookies can be parsed from response headers after first making a GET request to:

https://accounts.eu1.gigya.com/accounts.webSdkBootstrap?apiKey=3_t2Z1dFrbWR-IjcC-Bod1kei6W91UKmeiu3dETVG5iKaY4ILBRzVsmgRHWWo0fqqd&pageURL=https%3A%2F%2Fwww.videoland.com%2F&sdk=js_latest&sdkBuild=13455&format=json

With the following headers.

Host: accounts.eu1.gigya.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0
Accept: */*
Accept-Language: nl,en-US;q=0.7,en;q=0.3
Origin: https://cdns.eu1.gigya.com
DNT: 1
Connection: keep-alive
Referer: https://cdns.eu1.gigya.com/
Cookie: gig3pctest=true
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
Accept-Encoding: gzip, deflate

The response should contain the required cookies for login.

set-cookie: gmid=gmid.ver4.AcbHF_bQLg.qEdRRBNoJRoIWKw42-12Ms4pmka-Q0EV5eFZEsntAROdB0_gTfTEJN87Oy5S9CrH.5zK-lVztBN1Arvn7imJRW1aJbaitQKAQzYZ9JAeCwevj3vwvptLm47W3Z-58FX_z7_glMOCdmS_A85rnGUqKSA.sc3; expires=Sat, 18 Nov 2023 15:35:48 GMT; domain=.gigya.com; path=/; secure; samesite=none; httponly
set-cookie: ucid=Kx4yQiSc1ImzRcpmS59XKw; expires=Sat, 18 Nov 2023 15:35:48 GMT; domain=.gigya.com; path=/; secure; samesite=none
set-cookie: hasGmid=ver4; expires=Thu, 18 May 2023 14:35:48 GMT; domain=.gigya.com; path=/; secure; samesite=none

If someone is familair with Python, please edit the api.py so it parses the required cookies :)

Nigel1992 avatar Nov 18 '22 16:11 Nigel1992

Is this still an issue and does this fix still work? If others experience the same issues, I could implement this in my fork so everything could be in one place again. @Nigel1992 Static cookies doesn't seem the best idea as it could stop working any moment, response headers are easily obtained by, assume the response is called download: download.headers['the-required-header-name'] Ziggo uses response headers for tokens, too.

3DSBricker avatar Feb 01 '23 11:02 3DSBricker

@3DSBricker

This fix is not working anymore as the entire login procedure changed. See https://github.com/dut-iptv/dut-iptv/issues/144#issuecomment-1352449715 Sidenote: I did say to parse the cookies as static was only a temp fix 😉

Nigel1992 avatar Feb 01 '23 13:02 Nigel1992

I can't test anything since I don't have Videoland. I'm not sure whether the "PRIVATE" in the links represent a token, your UID (user ID), name or something else. I assumed it was the UID (of which I don't know the layout). I doubt it'll be as simple as I did, and it probably won't work (yet), but I did try to apply the V2 logic. Also, if your ID looks something like this: _puid_0dba....dbd18, delete the HTTP file. In one response you edited the token, but didn't in a later (renew) request. That token isn't important in terms of hacking as far as I know. Could you test this? If it doesn't work (at all), please make a new, full, dump, blurring all private information like passwords and addresses (I saw they even include bank details, fortunately they're incomplete). But do include the requests from clicking on the login button and all those things. I noticed the Widevine license mentioned in the request is only valid for up to 540P, did you watch "John Wick 2" in 540P?

plugin.video.videoland.zip

3DSBricker avatar Feb 16 '23 16:02 3DSBricker

@3DSBricker Thanks for your help. Unfortunately, it didn't work :( I'll make a new dump soon :)

Nigel1992 avatar Feb 26 '23 12:02 Nigel1992

@3DSBricker Contact me on [decode as base64] dGhlZGpza3l3YWxrZXJAZ21haWwuY29t and I will provide you with a valid account to get this problem sorted.

Nigel1992 avatar Apr 09 '23 22:04 Nigel1992