Guest - Testers are needed for the reworked CDateTime core component. See... https://forum.kodi.tv/showthread.php?tid=378981 (September 29) x
Addons development: how does Python handle cookies?
#1
I didn't find any location dedicated to addon development (is there any?).

Anyway, I need some help as I'm making circles since a few weeks now, working on Orange TV France for Kodi. No need to be in France or use Orange to understand this problem.

I'm working on a feature to allow people to use their Orange credentials so then can access TV when they're not at home. This is possible using both their mobile app and their website, so it should be doable. As for the rest of this addon, I'm replicating the behaviour of a web browser in order to do so. There're basically four steps:
  1. Load login page
  2. Post user email
  3. Post user password
  4. Retrieve auth token
Quite easy, but there is a trick: each call sends back a token I have to include in the next one as a cookie. As I'm more fluent in JS, I made a prototype with and I'm able to get the final auth token. So, that's scriptable. But I'm stuck with Kodi Python. Step 2 call keeps responding with 412. I ran a few tests and I'm able to reproduce that issue with Postman and JS: cookie is set in step 2 call, but the value is wrong.

I tried everything I could think of: cookie set into the headers, cookie set using HTTPCookieProcessor and CookieJar, etc. Result is still the same. What am I missing?
Windows 11 x64 | Kodi 21 | Developer of Orange TV France addon
Reply
#2
There is a section for adding development - I’ll move the thread there…
|Banned add-ons (wiki)|Forum rules (wiki)|VPN policy (wiki)|First time user (wiki)|FAQs (wiki) Troubleshooting (wiki)|Add-ons (wiki)|Free content (wiki)|Debug Log (wiki)|

Kodi Blog Posts
Reply
#3
Thanks!
Windows 11 x64 | Kodi 21 | Developer of Orange TV France addon
Reply
#4
My addon handles this by saving the cookie/login cred info that is returned into a json file in the userdata folder for the addon.

Here's a snippet of getting the cookie (yours will obviously be different and depends a lot on the sytem your logging into), I'm using request sessions to handle the request:
python:

with self.session.post(self.config.get('login_url'),verify=False,headers=self.login_headers,data=self.login_form_data) as r:
r.raise_for_status()
if r.ok and isinstance(r.text,str) and 'Successful login' in r.text:
self.logged_in = True
xbmc.log(msg='User is logged in',level=xbmc.LOGDEBUG)
if isinstance(r.cookies.get_dict(),dict) and all([x in r.cookies.get_dict().keys() for x in ['logged-in-sig', 'logged-in-user']]):
user_cookie = {'cookie':r.cookies.get_dict(),'expires':max([x.expires for x in r.cookies]),'domain':next(iter([x.domain for x in r.cookies]),None)}
self.config.files.get('user_cookie').write_text(json.dumps(user_cookie))
xbmc.log(msg='User session cookie set, expires at: {}'.format(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(user_cookie.get('expires')))),level=xbmc.LOGDEBUG)


And the next time the user opens the addon, i see if the cookie json file exists, and load it if the cookie hasn't expired, remove it if it has expired:
python:

def load_previous_cookie(self):
user_cookie = None
if self.config.files.get('user_cookie').exists():
try:
user_cookie = json.loads(self.config.files.get('user_cookie').read_text())
except Exception as exc:
xbmc.log(msg='Cookie read error: {}'.format(exc),level=xbmc.LOGERROR)
if isinstance(user_cookie,dict) and isinstance(user_cookie.get('cookie'),dict) and isinstance(user_cookie.get('expires'),int) and user_cookie.get('expires')>time.time():
for k,v in user_cookie.get('cookie').items():
self.session.cookies.set(k,v,domain=user_cookie.get('domain'))
xbmc.log(msg='Cookie loaded from previous session',level=xbmc.LOGDEBUG)
else:
if self.config.files.get('user_cookie').exists():
self.config.files.get('user_cookie').unlink()
xbmc.log(msg='Cookie deleted from previous session (expired or malformed)',level=xbmc.LOGDEBUG)
Reply
#5
Oh, quite interesting, I never thought I could actually store them into a file.

Where do you get self.session from? What object is it?

Thanks for your help!
Windows 11 x64 | Kodi 21 | Developer of Orange TV France addon
Reply
#6
I think it's a HTTP session object, probably from the Python's 'requests' library that many add-ons use (docs).
It's quite a big library with many dependencies, and I got fed up with the loading time that it added on my slow Android device, so I wrote my own lighter HTTP client with Python's built-in modules, like 'sockets' and 'ssl'.

---------

Many add-ons store data using custom files like zachmorris showed.
Since your usecase is a simple value like a boolean, integer or string, you could also use a hidden add-on setting as storage method. There's less code needed with this method.

For Kodi 18 Leia, you use a setting that has its "visible" attribute set to "false".
XML:
<!-- Using dots in the ID like with "user.cookies" is just a convention. Use anything that XML accepts. -->
<setting id="user.cookies" label="Hidden User Cookies" type="text" visible="false" default="" />
Then to access it:
Python:
from xbmcaddon import Addon
ADDON = Addon()
# Reading it (defaults to the default value set in the XML).
orangeTVCookies = ADDON.getSetting('user.cookies')
# Writing it.
ADDON.setSetting('user.cookies', orangeTVCookies + 'asdf')

For Kodi 19 Matrix and above, you need to use a "Level 4" setting, it's the same as an invisible setting as explained in here: https://kodi.wiki/view/Add-on_settings_conversion#Level

I don't use Kodi 19+ so I defer to MoojMidge's YouTube add-on on how they're defined:
https://github.com/anxdpanic/plugin.vide...ml#L11-L17
(If you search for "level>4" in that file you'll find other hidden settings that they use to store data.)

You can access those level 4 settings in the same way as with Kodi Leia, or also use the specialized functions for each data type (note that Kodi 19 and 20 went through some relevant changes to these functions):  
https://xbmc.github.io/docs.kodi.tv/mast...bd180af650

To "clear" a setting, either delete the entire "settings.xml" file in the /Kodi/userdata/my_addon_folder/ directory, or just set it to a ("") blank string, and react to it being empty in your Python code, like fetching a new cookie or whatever.

Edit: regarding the size limits of the contents that a setting can hold.
The Kodi source code says that it returns a std::string value: https://github.com/xbmc/xbmc/blob/master...#L126-L133
And the maximum size of that comes from string::max_size from C++ (a huge value), so assume that your hidden settings can only be as big as a piece of the device RAM -- lowball it like 32 MB or something (which is quite a big string).
Reply
#7
I finally found a way using HTTPSConnection. At the end of the login process I'm retrieving a few tokens I can finally use to get video streams. I pretty sure I used that before, so I guess I made thing cleaner this time.

I read a lot about that requests library, I didn't know I could actually use it within Kodi. How do I tell Kodi to load a library I would normally fetch with pip?

I never thought about storing data within settings either, that's very cleaver. And I just understood about settings levels. I learnt so much in two posts guys, big thanks, really. That made my day!
Windows 11 x64 | Kodi 21 | Developer of Orange TV France addon
Reply
#8
(2024-09-29, 01:05)BreizhReloaded Wrote: I read a lot about that requests library, I didn't know I could actually use it within Kodi. How do I tell Kodi to load a library I would normally fetch with pip?
You have a bunch of modules that come bundled with the Python interpreter in Kodi already, but for third party libraries, you either need to copy their code files to distribute them with your add-on (literally copying the source directory to your add-on resources, so you can import it from your scripts), or rely on script add-ons that people have created.

For example, Requests is packed as script.module.requests, a code library add-on that must be added as a dependency in your addon.xml manifest so your add-on can "see" it and be able to import its contents, like this:
https://github.com/anxdpanic/plugin.vide....xml#L3-L9
It doesn't come with Kodi out of the box, so when someone installs your add-on, Kodi will try to resolve its dependencies and download them as well, like that Requests script add-on (which depends on other script add-ons too, like script.module.urllib3 etc).

More in here:
https://kodi.wiki/view/Addon.xml#%3Crequires%3E
Reply
#9
(2024-09-29, 01:05)BreizhReloaded Wrote: I never thought about storing data within settings either, that's very cleaver. And I just understood about settings levels. I learnt so much in two posts guys, big thanks, really. That made my day!

One other trick, if you run into cookies or similar that are large or may be hard to store in Kodi settings you can use pickle.  I do it for storing UPnP server data in Kodi settings:

settings with pickle:
def settings(setting, value = None):
    # Get or add addon setting
    if value:
        xbmcaddon.Addon().setSetting(setting, value)
    else:
        return xbmcaddon.Addon().getSetting(setting)  


saved_servers = settings('saved_servers')
if len(saved_servers) < 5 or saved_servers == 'none' or force:
    srvrlist = ssdp.discover("urnConfusedchemas-upnp-org:device:MediaServer:1", timeout=timeoutval)
    servers = onlyDiscMezzmo(srvrlist)
    # save the servers for faster loading
    settings('saved_servers', pickle.dumps(servers,0,fix_imports=True))
else:
    saved_servers = saved_servers.encode('utf-8')
    servers = pickle.loads(saved_servers, fix_imports=True)

Thanks,

Jeff
Running with the Mezzmo Kodi addon.  The easier way to share your media with multiple Kodi clients.
Service.autostop , CBC Sports, Kodi Selective Cleaner and Mezzmo Kodi addon author.
Reply
#10
Wonderful. That will drive me to some code refactoring I guess Big Grin

Thank you for all your inputs!
Windows 11 x64 | Kodi 21 | Developer of Orange TV France addon
Reply
#11
Orange TV France now uses requests, and stores session data so it doesn't re-authenticate with Orange each time it opens a video stream (much much faster).

Thank you again guys!
Windows 11 x64 | Kodi 21 | Developer of Orange TV France addon
Reply
#12
IMO the best way to store cookies is a pickle file inside addon's profile directory. Unlike Python's built-in CookieJar class request's CookieJar is picklable and can be easily saved. A minimal code example:
Code:

import pickle
from pathlib import Path

import requests
import xbmcvfs
from xbmcaddon import Addon

PROFILE_DIR = Path(xbmcvfs.translatePath(Addon().getAddonInfo('profile')))
COOKIE_FILE = PROFILE_DIR / 'cookies.pickle'

cookie_jar = None
if COOKIE_FILE.exists():
    with COOKIE_FILE.open('rb') as fo:
        cookie_jar = pickle.load(fo)

response = requests.get('https://example.com', cookies=cookie_jar)

with COOKIE_FILE.open('wb') as fo:
    pickle.dump(response.cookies, fo)

A bit of self-promotion Smile Since requests is a quite heavy library with several dependencies and may slow down addon loading on weaker systems like single-boards I've written my own mini-library that uses urllib.request and provides API similar to that of requests: https://gist.github.com/romanvm/69142342...8a201c091b
Reply
#13
it  would be simpler to save cookies in a json format and add them to the headers

see example here https://www.pythonrequests.com/python-requests-cookies/
Reply
#14
Cookies have many properties beyond name and value. So it's better to use specialized solutions. Also in case of requests no need to play with headers, requests can accept cookies as dict.
Reply
#15
(2024-10-14, 18:19)Roman_V_M Wrote: Cookies have many properties beyond name and value.

yes. https://datatracker.ietf.org/doc/html/rfc6265

BUT we're not manipulating cookies, simply storing what's received and sending them back so a simple solution works best
 
(2024-10-14, 18:19)Roman_V_M Wrote: Also in case of requests no need to play with headers, requests can accept cookies as dict.

it still does it behind the scenes even if you don't interact with it directly, might as well skip a step and know how to do it in your own code and remove reliance on someone else's code

but at the same time i personally am the type to go with raw sockets over requests anyway so my way of doing things is already not the common way
Reply

Logout Mark Read Team Forum Stats Members Help
Addons development: how does Python handle cookies?0