2ba0cbb9bd4722dcfcf3b7e071e509ea60a0dd17
[plugin.video.netflix.git] / resources / lib / NetflixSession.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # Module: NetflixSession
4 # Created on: 13.01.2017
5
6 import sys
7 import os
8 import base64
9 import time
10 import urllib
11 import json
12 import requests
13 try:
14    import cPickle as pickle
15 except:
16    import pickle
17 from bs4 import BeautifulSoup
18 from utils import strip_tags
19 from utils import noop
20
21 class NetflixSession:
22     """Helps with login/session management of Netflix users & API data fetching"""
23
24     base_url = 'https://www.netflix.com'
25     """str: Secure Netflix url"""
26
27     urls = {
28         'login': '/login',
29         'browse': '/browse',
30         'video_list_ids': '/warmer',
31         'shakti': '/pathEvaluator',
32         'profiles':  '/profiles',
33         'switch_profiles': '/profiles/switch',
34         'adult_pin': '/pin/service',
35         'metadata': '/metadata',
36         'set_video_rating': '/setVideoRating',
37         'update_my_list': '/playlistop'
38     }
39     """:obj:`dict` of :obj:`str` List of all static endpoints for HTML/JSON POST/GET requests"""
40
41     video_list_keys = ['user', 'genres', 'recommendations']
42     """:obj:`list` of :obj:`str` Divide the users video lists into 3 different categories (for easier digestion)"""
43
44     profiles = {}
45     """:obj:`dict`
46         Dict of user profiles, user id is the key:
47
48         "72ERT45...": {
49             "profileName": "username",
50             "avatar": "http://..../avatar.png",
51             "id": "72ERT45...",
52             "isAccountOwner": False,
53             "isActive": True,
54             "isFirstUse": False
55         }
56     """
57
58     user_data = {}
59     """:obj:`dict`
60         dict of user data (used for authentication):
61
62         {
63             "guid": "72ERT45...",
64             "authURL": "145637....",
65             "countryOfSignup": "DE",
66             "emailAddress": "foo@..",
67             "gpsModel": "harris",
68             "isAdultVerified": True,
69             "isInFreeTrial": False,
70             "isKids": False,
71             "isTestAccount": False,
72             "numProfiles": 5,
73             "pinEnabled": True
74         }
75     """
76
77     api_data = {}
78     """:obj:`dict`
79         dict of api data (used to build up the api urls):
80
81         {
82             "API_BASE_URL": "/shakti",
83             "API_ROOT": "https://www.netflix.com/api",
84             "BUILD_IDENTIFIER": "113b89c9", "
85             ICHNAEA_ROOT": "/ichnaea"
86         }
87     """
88
89     esn = ''
90     """str: Widevine esn, something like: NFCDCH-MC-D7D6F54LOPY8J416T72MQXX3RD20ME"""
91
92     def __init__(self, cookie_path, data_path, verify_ssl=True, log_fn=noop):
93         """Stores the cookie path for later use & instanciates a requests
94            session with a proper user agent & stored cookies/data if available
95
96         Parameters
97         ----------
98         cookie_path : :obj:`str`
99             Cookie location
100
101         data_path : :obj:`str`
102             User data cache location
103
104         log_fn : :obj:`fn`
105              optional log function
106         """
107         self.cookie_path = cookie_path
108         self.data_path = data_path
109         self.verify_ssl = verify_ssl
110         self.log = log_fn
111
112         # start session, fake chrome (so that we get a proper widevine esn) & enable gzip
113         self.session = requests.session()
114         self.session.headers.update({
115             'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36',
116             'Accept-Encoding': 'gzip, deflate'
117         })
118
119     def parse_login_form_fields (self, form_soup):
120         """Fetches all the inputfields from the login form, so that we
121            can build a request with all the fields needed besides the known email & password ones
122
123         Parameters
124         ----------
125         form_soup : :obj:`BeautifulSoup`
126             Instance of an BeautifulSoup documet or node containing the login form
127
128         Returns
129         -------
130             :obj:`dict` of :obj:`str`
131                 Dictionary of all input fields with their name as the key & the default
132                 value from the form field
133         """
134         login_input_fields = {}
135         login_inputs = form_soup.find_all('input')
136         # gather all form fields, set an empty string as the default value
137         for item in login_inputs:
138             keys = dict(item.attrs).keys()
139             if 'name' in keys and 'value' not in keys:
140                 login_input_fields[item['name']] = ''
141             elif 'name' in keys and 'value' in keys:
142                 login_input_fields[item['name']] = item['value']
143         return login_input_fields
144
145     def extract_inline_netflix_page_data (self, page_soup):
146         """Extracts all <script/> tags from the given document and parses the contents of each one of `em.
147         The contents of the parsable tags looks something like this:
148
149             <script>window.netflix = window.netflix || {} ;
150                     netflix.notification = {"constants":{"sessionLength":30,"ownerToken":"ZDD...};</script>
151
152         So we´re extracting every JavaScript object contained in the `netflix.x = {};` variable,
153         strip all html tags, unescape the whole thing & finally parse the resulting serialized JSON from this
154         operations. Errors are expected, as not all <script/> tags contained in the page follow these pattern,
155         but the ones we need do, so we´re just catching any errors and applying a noop() function in case this happens,
156         as we´re not interested in those.
157
158         Note: Yes this is ugly & I´d like to avoid doing this, but Netflix leaves us no other choice,
159             as there are simply no api endpoints for the data, we need to extract them from HTML,
160             or better, JavaScript as we´re parsing the contents of <script/> tags
161
162         Parameters
163         ----------
164         page_soup : :obj:`BeautifulSoup`
165             Instance of an BeautifulSoup document or node containing the complete page contents
166
167         Returns
168         -------
169             :obj:`list` of :obj:`dict`
170                 List of all the serialized data pulled out of the pagws <script/> tags
171         """
172         inline_data = [];
173         data_scripts = page_soup.find_all('script', attrs={'src': None});
174         for script in data_scripts:
175             # ugly part: try to parse the data & don't care about errors (as they will be some)
176             try:
177                 # find the first occurance of the 'netflix.' string, assigning the contents to a global js var
178                 str_index = str(script).find('netflix.')
179                 # filter out the contents between the 'netflix.x =' & ';<script>'
180                 stripped_data = str(script)[str_index:][(str(script)[str_index:].find('= ') + 2):].replace(';</script>', '').strip()
181                 # unescape the contents as they contain characters a JSON parser chokes up upon
182                 unescaped_data = stripped_data.decode('string_escape')
183                 # strip all the HTML tags within the strings a JSON parser chokes up upon them
184                 transformed_data = strip_tags(unescaped_data)
185                 # parse the contents with a regular JSON parser, as they should be in a shape that ot actually works
186                 try:
187                     parsed_data = json.loads(transformed_data)
188                     inline_data.append(parsed_data)
189                 except ValueError, e:
190                     noop()
191             except TypeError, e:
192                 noop()
193
194         return inline_data;
195
196     def _parse_user_data (self, netflix_page_data):
197         """Parse out the user data from the big chunk of dicts we got from
198            parsing the JSON-ish data from the netflix homepage
199
200         Parameters
201         ----------
202         netflix_page_data : :obj:`list`
203             List of all the JSON-ish data that has been extracted from the Netflix homepage
204             see: extract_inline_netflix_page_data
205
206         Returns
207         -------
208             :obj:`dict` of :obj:`str`
209
210             {
211                 "guid": "72ERT45...",
212                 "authURL": "145637....",
213                 "countryOfSignup": "DE",
214                 "emailAddress": "foo@..",
215                 "gpsModel": "harris",
216                 "isAdultVerified": True,
217                 "isInFreeTrial": False,
218                 "isKids": False,
219                 "isTestAccount": False,
220                 "numProfiles": 5,
221                 "pinEnabled": True
222             }
223         """
224         user_data = {};
225         important_fields = [
226             'authURL',
227             'countryOfSignup',
228             'emailAddress',
229             'gpsModel',
230             'guid',
231             'isAdultVerified',
232             'isInFreeTrial',
233             'isKids',
234             'isTestAccount',
235             'numProfiles',
236             'pinEnabled'
237         ]
238         for item in netflix_page_data:
239             if 'models' in dict(item).keys():
240                 for important_field in important_fields:
241                     user_data.update({important_field: item['models']['userInfo']['data'][important_field]})
242         return user_data
243
244     def _parse_profile_data (self, netflix_page_data):
245         """Parse out the profile data from the big chunk of dicts we got from
246            parsing the JSON-ish data from the netflix homepage
247
248         Parameters
249         ----------
250         netflix_page_data : :obj:`list`
251             List of all the JSON-ish data that has been extracted from the Netflix homepage
252             see: extract_inline_netflix_page_data
253
254         Returns
255         -------
256             :obj:`dict` of :obj:`dict
257
258             {
259                 "72ERT45...": {
260                     "profileName": "username",
261                     "avatar": "http://..../avatar.png",
262                     "id": "72ERT45...",
263                     "isAccountOwner": False,
264                     "isActive": True,
265                     "isFirstUse": False
266                 }
267             }
268         """
269         profiles = {};
270         important_fields = [
271             'profileName',
272             'isActive',
273             'isFirstUse',
274             'isAccountOwner'
275         ]
276         # TODO: get rid of this christmas tree of doom
277         for item in netflix_page_data:
278             if 'profiles' in dict(item).keys():
279                 for profile_id in item['profiles']:
280                     if self._is_size_key(key=profile_id) == False:
281                         profile = {'id': profile_id}
282                         for important_field in important_fields:
283                             profile.update({important_field: item['profiles'][profile_id]['summary'][important_field]})
284                         profile.update({'avatar': item['avatars']['nf'][item['profiles'][profile_id]['summary']['avatarName']]['images']['byWidth']['320']['value']})
285                         profiles.update({profile_id: profile})
286
287         return profiles
288
289     def _parse_api_base_data (self, netflix_page_data):
290         """Parse out the api url data from the big chunk of dicts we got from
291            parsing the JSOn-ish data from the netflix homepage
292
293         Parameters
294         ----------
295         netflix_page_data : :obj:`list`
296             List of all the JSON-ish data that has been extracted from the Netflix homepage
297             see: extract_inline_netflix_page_data
298
299         Returns
300         -------
301             :obj:`dict` of :obj:`str
302
303             {
304                 "API_BASE_URL": "/shakti",
305                 "API_ROOT": "https://www.netflix.com/api",
306                 "BUILD_IDENTIFIER": "113b89c9", "
307                 ICHNAEA_ROOT": "/ichnaea"
308             }
309         """
310         api_data = {};
311         important_fields = [
312             'API_BASE_URL',
313             'API_ROOT',
314             'BUILD_IDENTIFIER',
315             'ICHNAEA_ROOT'
316         ]
317         for item in netflix_page_data:
318             if 'models' in dict(item).keys():
319                 for important_field in important_fields:
320                     api_data.update({important_field: item['models']['serverDefs']['data'][important_field]})
321         return api_data
322
323     def _parse_esn_data (self, netflix_page_data):
324         """Parse out the esn id data from the big chunk of dicts we got from
325            parsing the JSOn-ish data from the netflix homepage
326
327         Parameters
328         ----------
329         netflix_page_data : :obj:`list`
330             List of all the JSON-ish data that has been extracted from the Netflix homepage
331             see: extract_inline_netflix_page_data
332
333         Returns
334         -------
335             :obj:`str` of :obj:`str
336             Widevine esn, something like: NFCDCH-MC-D7D6F54LOPY8J416T72MQXX3RD20ME
337         """
338         esn = '';
339         for item in netflix_page_data:
340             if 'models' in dict(item).keys():
341                 esn = item['models']['esnGeneratorModel']['data']['esn']
342         return esn
343
344     def _parse_page_contents (self, page_soup):
345         """Call all the parsers we need to extract all the session relevant data from the HTML page
346            Directly assigns it to the NetflixSession instance
347
348         Parameters
349         ----------
350         page_soup : :obj:`BeautifulSoup`
351             Instance of an BeautifulSoup document or node containing the complete page contents
352         """
353         netflix_page_data = self.extract_inline_netflix_page_data(page_soup=page_soup)
354         self.user_data = self._parse_user_data(netflix_page_data=netflix_page_data)
355         self.esn = self._parse_esn_data(netflix_page_data=netflix_page_data)
356         self.api_data = self._parse_api_base_data(netflix_page_data=netflix_page_data)
357         self.profiles = self._parse_profile_data(netflix_page_data=netflix_page_data)
358
359     def is_logged_in (self, account):
360         """Determines if a user is already logged in (with a valid cookie),
361            by fetching the index page with the current cookie & checking for the
362            `membership status` user data
363
364         Parameters
365         ----------
366         account : :obj:`dict` of :obj:`str`
367             Dict containing an email, country & a password property
368
369         Returns
370         -------
371         bool
372             User is already logged in (e.g. Cookie is valid) or not
373         """
374         is_logged_in = False
375         # load cookies
376         account_hash = self._generate_account_hash(account=account)
377         if self._load_cookies(filename=self.cookie_path + '_' + account_hash) == False:
378             return False
379         if self._load_data(filename=self.data_path + '_' + account_hash) == False:
380             # load the profiles page (to verify the user)
381             response = self.session.get(self._get_document_url_for(component='profiles'), verify=self.verify_ssl)
382
383             # parse out the needed inline information
384             page_soup = BeautifulSoup(response.text)
385             page_data = self.extract_inline_netflix_page_data(page_soup=page_soup)
386             self._parse_page_contents(page_soup=page_soup)
387
388             # check if the cookie is still valid
389             for item in page_data:
390                 if 'profilesList' in dict(item).keys():
391                     if item['profilesList']['summary']['length'] >= 1:
392                         is_logged_in = True
393             return is_logged_in
394         return True
395
396     def logout (self):
397         """Delete all cookies and session data
398
399         Parameters
400         ----------
401         account : :obj:`dict` of :obj:`str`
402             Dict containing an email, country & a password property
403
404         """
405         self._delete_cookies(path=self.cookie_path)
406         self._delete_data(path=self.data_path)
407
408     def login (self, account):
409         """Try to log in a user with its credentials & stores the cookies if the action is successfull
410
411            Note: It fetches the HTML of the login page to extract the fields of the login form,
412            again, this is dirty, but as the fields & their values coudl change at any time, this
413            should be the most reliable way of retrieving the information
414
415         Parameters
416         ----------
417         account : :obj:`dict` of :obj:`str`
418             Dict containing an email, country & a password property
419
420         Returns
421         -------
422         bool
423             User could be logged in or not
424         """
425         response = self.session.get(self._get_document_url_for(component='login'), verify=self.verify_ssl)
426         if response.status_code != 200:
427             return False;
428
429         # collect all the login fields & their contents and add the user credentials
430         page_soup = BeautifulSoup(response.text)
431         login_form = page_soup.find(attrs={'class' : 'ui-label-text'}).findPrevious('form')
432         login_payload = self.parse_login_form_fields(form_soup=login_form)
433         if 'email' in login_payload:
434             login_payload['email'] = account['email']
435         if 'emailOrPhoneNumber' in login_payload:
436             login_payload['emailOrPhoneNumber'] = account['email']
437         login_payload['password'] = account['password']
438
439         # perform the login
440         login_response = self.session.post(self._get_document_url_for(component='login'), data=login_payload, verify=self.verify_ssl)
441         login_soup = BeautifulSoup(login_response.text)
442
443         # we know that the login was successfull if we find an HTML element with the class of 'profile-name'
444         if login_soup.find(attrs={'class' : 'profile-name'}) or login_soup.find(attrs={'class' : 'profile-icon'}):
445             # parse the needed inline information & store cookies for later requests
446             self._parse_page_contents(page_soup=login_soup)
447             account_hash = self._generate_account_hash(account=account)
448             self._save_cookies(filename=self.cookie_path + '_' + account_hash)
449             self._save_data(filename=self.data_path + '_' + account_hash)
450             return True
451         else:
452             return False
453
454     def switch_profile (self, profile_id, account):
455         """Switch the user profile based on a given profile id
456
457         Note: All available profiles & their ids can be found in the ´profiles´ property after a successfull login
458
459         Parameters
460         ----------
461         profile_id : :obj:`str`
462             User profile id
463
464         account : :obj:`dict` of :obj:`str`
465             Dict containing an email, country & a password property
466
467         Returns
468         -------
469         bool
470             User could be switched or not
471         """
472         payload = {
473             'switchProfileGuid': profile_id,
474             '_': int(time.time()),
475             'authURL': self.user_data['authURL']
476         }
477
478         response = self.session.get(self._get_api_url_for(component='switch_profiles'), params=payload, verify=self.verify_ssl);
479         if response.status_code != 200:
480             return False
481
482         # fetch the index page again, so that we can fetch the corresponding user data
483         browse_response = self.session.get(self._get_document_url_for(component='browse'), verify=self.verify_ssl)
484         browse_soup = BeautifulSoup(browse_response.text)
485         self._parse_page_contents(page_soup=browse_soup)
486         account_hash = self._generate_account_hash(account=account)
487         self._save_data(filename=self.data_path + '_' + account_hash)
488         return True
489
490     def send_adult_pin (self, pin):
491         """Send the adult pin to Netflix in case an adult rated video requests it
492
493         Note: Once entered, it should last for the complete session (Not so sure about this)
494
495         Parameters
496         ----------
497         pin : :obj:`str`
498             The users adult pin
499
500         Returns
501         -------
502         bool
503             Pin was accepted or not
504         or
505         :obj:`dict` of :obj:`str`
506             Api call error
507         """
508         payload = {
509             'pin': pin,
510             'authURL': self.user_data['authURL']
511         }
512         url = self._get_api_url_for(component='adult_pin')
513         response = self.session.get(url, params=payload, verify=self.verify_ssl);
514         pin_response = self._process_response(response=response, component=url)
515         keys = pin_response.keys()
516         if 'success' in keys:
517             return True
518         if 'error' in keys:
519             return pin_response
520         return False
521
522     def add_to_list (self, video_id):
523         """Adds a video to "my list" on Netflix
524
525         Parameters
526         ----------
527         video_id : :obj:`str`
528             ID of th show/video/movie to be added
529
530         Returns
531         -------
532         bool
533             Adding was successfull
534         """
535         return self._update_my_list(video_id=video_id, operation='add')
536
537     def remove_from_list (self, video_id):
538         """Removes a video from "my list" on Netflix
539
540         Parameters
541         ----------
542         video_id : :obj:`str`
543             ID of th show/video/movie to be removed
544
545         Returns
546         -------
547         bool
548             Removing was successfull
549         """
550         return self._update_my_list(video_id=video_id, operation='remove')
551
552     def rate_video (self, video_id, rating):
553         """Rate a video on Netflix
554
555         Parameters
556         ----------
557         video_id : :obj:`str`
558             ID of th show/video/movie to be rated
559
560         rating : :obj:`int`
561             Rating, must be between 0 & 10
562
563         Returns
564         -------
565         bool
566             Rating successfull or not
567         """
568
569         # dirty rating validation
570         ratun = int(rating)
571         if rating > 10 or rating < 0:
572             return False
573
574         # In opposition to Kodi, Netflix uses a rating from 0 to in 0.5 steps
575         if rating != 0:
576             rating = rating / 2
577
578         headers = {
579             'Content-Type': 'application/json',
580             'Accept': 'application/json, text/javascript, */*',
581         }
582
583         params = {
584             'titleid': video_id,
585             'rating': rating
586         }
587
588         payload = json.dumps({
589             'authURL': self.user_data['authURL']
590         })
591
592         response = self.session.post(self._get_api_url_for(component='set_video_rating'), params=params, headers=headers, data=payload, verify=self.verify_ssl)
593         return response.status_code == 200
594
595     def parse_video_list_ids (self, response_data):
596         """Parse the list of video ids e.g. rip out the parts we need
597
598         Parameters
599         ----------
600         response_data : :obj:`dict` of :obj:`str`
601             Parsed response JSON from the ´fetch_video_list_ids´ call
602
603         Returns
604         -------
605         :obj:`dict` of :obj:`dict`
606             Video list ids in the format:
607
608             {
609                 "genres": {
610                     "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568367": {
611                         "displayName": "US-Serien",
612                         "id": "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568367",
613                         "index": 3,
614                         "name": "genre",
615                         "size": 38
616                     },
617                     "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568368": {
618                         "displayName": ...
619                     },
620                 },
621                 "user": {
622                     "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568364": {
623                         "displayName": "Meine Liste",
624                         "id": "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568364",
625                         "index": 0,
626                         "name": "queue",
627                         "size": 2
628                     },
629                     "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568365": {
630                         "displayName": ...
631                     },
632                 },
633                 "recommendations": {
634                     "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568382": {
635                         "displayName": "Passend zu Family Guy",
636                         "id": "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568382",
637                         "index": 18,
638                         "name": "similars",
639                         "size": 33
640                     },
641                     "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568397": {
642                         "displayName": ...
643                     }
644                 }
645             }
646         """
647         # prepare the return dictionary
648         video_list_ids = {}
649         for key in self.video_list_keys:
650             video_list_ids[key] = {}
651
652         # subcatogorize the lists by their context
653         video_lists = response_data['lists']
654         for video_list_id in video_lists.keys():
655             video_list = video_lists[video_list_id]
656             if video_list['context'] == 'genre':
657                 video_list_ids['genres'].update(self.parse_video_list_ids_entry(id=video_list_id, entry=video_list))
658             elif video_list['context'] == 'similars' or video_list['context'] == 'becauseYouAdded':
659                 video_list_ids['recommendations'].update(self.parse_video_list_ids_entry(id=video_list_id, entry=video_list))
660             else:
661                 video_list_ids['user'].update(self.parse_video_list_ids_entry(id=video_list_id, entry=video_list))
662
663         return video_list_ids
664
665     def parse_video_list_ids_entry (self, id, entry):
666         """Parse a video id entry e.g. rip out the parts we need
667
668         Parameters
669         ----------
670         response_data : :obj:`dict` of :obj:`str`
671             Dictionary entry from the ´fetch_video_list_ids´ call
672
673         Returns
674         -------
675         id : :obj:`str`
676             Unique id of the video list
677
678         entry : :obj:`dict` of :obj:`str`
679             Video list entry in the format:
680
681             "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568382": {
682                 "displayName": "Passend zu Family Guy",
683                 "id": "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568382",
684                 "index": 18,
685                 "name": "similars",
686                 "size": 33
687             }
688         """
689         return {
690             id: {
691                 'id': id,
692                 'index': entry['index'],
693                 'name': entry['context'],
694                 'displayName': entry['displayName'],
695                 'size': entry['length']
696             }
697         }
698
699     def parse_search_results (self, response_data):
700         """Parse the list of search results, rip out the parts we need
701            and extend it with detailed show informations
702
703         Parameters
704         ----------
705         response_data : :obj:`dict` of :obj:`str`
706             Parsed response JSON from the `fetch_search_results` call
707
708         Returns
709         -------
710         :obj:`dict` of :obj:`dict` of :obj:`str`
711             Search results in the format:
712
713             {
714                 "70136140": {
715                     "boxarts": "https://art-s.nflximg.net/0d7af/d5c72668c35d3da65ae031302bd4ae1bcc80d7af.jpg",
716                     "detail_text": "Die legend\u00e4re und mit 13 Emmys nominierte Serie von Gene Roddenberry inspirierte eine ganze Generation.",
717                     "id": "70136140",
718                     "season_id": "70109435",
719                     "synopsis": "Unter Befehl von Captain Kirk begibt sich die Besatzung des Raumschiffs Enterprise in die Tiefen des Weltraums, wo sie fremde Galaxien und neue Zivilisationen erforscht.",
720                     "title": "Star Trek",
721                     "type": "show"
722                 },
723                 "70158329": {
724                     "boxarts": ...
725                 }
726             }
727         """
728         search_results = {}
729         raw_search_results = response_data['value']['videos']
730         for entry_id in raw_search_results:
731             if self._is_size_key(key=entry_id) == False:
732                 # fetch information about each show & build up a proper search results dictionary
733                 show = self.parse_show_list_entry(id=entry_id, entry=raw_search_results[entry_id])
734                 show[entry_id].update(self.parse_show_information(id=entry_id, response_data=self.fetch_show_information(id=entry_id, type=show[entry_id]['type'])))
735                 search_results.update(show)
736         return search_results
737
738     def parse_show_list_entry (self, id, entry):
739         """Parse a show entry e.g. rip out the parts we need
740
741         Parameters
742         ----------
743         response_data : :obj:`dict` of :obj:`str`
744             Dictionary entry from the ´fetch_show_information´ call
745
746         id : :obj:`str`
747             Unique id of the video list
748
749         Returns
750         -------
751         entry : :obj:`dict` of :obj:`dict` of :obj:`str`
752             Show list entry in the format:
753
754             {
755                 "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568382": {
756                     "id": "3589e2c6-ca3b-48b4-a72d-34f2c09ffbf4_11568382",
757                     "title": "Enterprise",
758                     "boxarts": "https://art-s.nflximg.net/.../smth.jpg",
759                     "type": "show"
760                 }
761             }
762         """
763         return {
764             id: {
765                 'id': id,
766                 'title': entry['title'],
767                 'boxarts': entry['boxarts']['_342x192']['jpg']['url'],
768                 'type': entry['summary']['type']
769             }
770         }
771
772     def parse_video_list (self, response_data):
773         """Parse a list of videos
774
775         Parameters
776         ----------
777         response_data : :obj:`dict` of :obj:`str`
778             Parsed response JSON from the `fetch_video_list` call
779
780         Returns
781         -------
782         :obj:`dict` of :obj:`dict`
783             Video list in the format:
784
785             {
786                 "372203": {
787                     "artwork": null,
788                     "boxarts": {
789                       "big": "https://art-s.nflximg.net/5e7d3/b3b48749843fd3a36db11c319ffa60f96b55e7d3.jpg",
790                       "small": "https://art-s.nflximg.net/57543/a039845c2eb9186dc26019576d895bf5a1957543.jpg"
791                     },
792                     "cast": [
793                       "Christine Elise",
794                       "Brad Dourif",
795                       "Grace Zabriskie",
796                       "Jenny Agutter",
797                       "John Lafia",
798                       "Gerrit Graham",
799                       "Peter Haskell",
800                       "Alex Vincent",
801                       "Beth Grant"
802                     ],
803                     "creators": [],
804                     "directors": [],
805                     "episode_count": null,
806                     "genres": [
807                       "Horrorfilme"
808                     ],
809                     "id": "372203",
810                     "in_my_list": true,
811                     "interesting_moment": "https://art-s.nflximg.net/09544/ed4b3073394b4469fb6ec22b9df81a4f5cb09544.jpg",
812                     "list_id": "9588df32-f957-40e4-9055-1f6f33b60103_46891306",
813                     "maturity": {
814                       "board": "FSK",
815                       "description": "Nur f\u00fcr Erwachsene geeignet.",
816                       "level": 1000,
817                       "value": "18"
818                     },
819                     "quality": "540",
820                     "rating": 3.1707757,
821                     "regular_synopsis": "Ein Spielzeughersteller erweckt aus Versehen die Seele der M\u00f6rderpuppe Chucky erneut zum Leben, die sich unmittelbar wieder ihren m\u00f6rderischen Aktivit\u00e4ten zuwendet.",
822                     "runtime": 5028,
823                     "seasons_count": null,
824                     "seasons_label": null,
825                     "synopsis": "Die allseits beliebte, von D\u00e4monen besessene M\u00f6rderpuppe ist wieder da und verbreitet erneut Horror und Schrecken.",
826                     "tags": [
827                       "Brutal",
828                       "Spannend"
829                     ],
830                     "title": "Chucky 2 \u2013 Die M\u00f6rderpuppe ist wieder da",
831                     "type": "movie",
832                     "watched": false,
833                     "year": 1990
834                 },
835                 "80011356": {
836                     "artwork": null,
837                     "boxarts": {
838                       "big": "https://art-s.nflximg.net/7c10d/5dcc3fc8f08487e92507627068cfe26ef727c10d.jpg",
839                       "small": "https://art-s.nflximg.net/5bc0e/f3be361b8c594929062f90a8d9c6eb57fb75bc0e.jpg"
840                     },
841                     "cast": [
842                       "Bjarne M\u00e4del"
843                     ],
844                     "creators": [],
845                     "directors": [
846                       "Arne Feldhusen"
847                     ],
848                     "episode_count": 24,
849                     "genres": [
850                       "Deutsche Serien",
851                       "Serien",
852                       "Comedyserien"
853                     ],
854                     "id": "80011356",
855                     "in_my_list": true,
856                     "interesting_moment": "https://art-s.nflximg.net/0188e/19cd705a71ee08c8d2609ae01cd8a97a86c0188e.jpg",
857                     "list_id": "9588df32-f957-40e4-9055-1f6f33b60103_46891306",
858                     "maturity": {
859                       "board": "FSF",
860                       "description": "Geeignet ab 12 Jahren.",
861                       "level": 80,
862                       "value": "12"
863                     },
864                     "quality": "720",
865                     "rating": 4.4394655,
866                     "regular_synopsis": "Comedy-Serie \u00fcber die Erlebnisse eines Tatortreinigers, der seine schmutzige Arbeit erst beginnen kann, wenn die Polizei die Tatortanalyse abgeschlossen hat.",
867                     "runtime": null,
868                     "seasons_count": 5,
869                     "seasons_label": "5 Staffeln",
870                     "synopsis": "In den meisten Krimiserien werden Mordf\u00e4lle auf faszinierende und spannende Weise gel\u00f6st. Diese Serie ist anders.",
871                     "tags": [
872                       "Zynisch"
873                     ],
874                     "title": "Der Tatortreiniger",
875                     "type": "show",
876                     "watched": false,
877                     "year": 2015
878                 },
879             }
880         """
881         video_list = {};
882         raw_video_list = response_data['value']
883         netflix_list_id = self.parse_netflix_list_id(video_list=raw_video_list);
884         for video_id in raw_video_list['videos']:
885             if self._is_size_key(key=video_id) == False:
886                 video_list.update(self.parse_video_list_entry(id=video_id, list_id=netflix_list_id, video=raw_video_list['videos'][video_id], persons=raw_video_list['person'], genres=raw_video_list['genres']))
887         return video_list
888
889     def parse_video_list_entry (self, id, list_id, video, persons, genres):
890         """Parse a video list entry e.g. rip out the parts we need
891
892         Parameters
893         ----------
894         id : :obj:`str`
895             Unique id of the video
896
897         list_id : :obj:`str`
898             Unique id of the containing list
899
900         video : :obj:`dict` of :obj:`str`
901             Video entry from the ´fetch_video_list´ call
902
903         persons : :obj:`dict` of :obj:`dict` of :obj:`str`
904             List of persons with reference ids
905
906         persons : :obj:`dict` of :obj:`dict` of :obj:`str`
907             List of genres with reference ids
908
909         Returns
910         -------
911         entry : :obj:`dict` of :obj:`dict` of :obj:`str`
912             Video list entry in the format:
913
914            {
915               "372203": {
916                 "artwork": null,
917                 "boxarts": {
918                   "big": "https://art-s.nflximg.net/5e7d3/b3b48749843fd3a36db11c319ffa60f96b55e7d3.jpg",
919                   "small": "https://art-s.nflximg.net/57543/a039845c2eb9186dc26019576d895bf5a1957543.jpg"
920                 },
921                 "cast": [
922                   "Christine Elise",
923                   "Brad Dourif",
924                   "Grace Zabriskie",
925                   "Jenny Agutter",
926                   "John Lafia",
927                   "Gerrit Graham",
928                   "Peter Haskell",
929                   "Alex Vincent",
930                   "Beth Grant"
931                 ],
932                 "creators": [],
933                 "directors": [],
934                 "episode_count": null,
935                 "genres": [
936                   "Horrorfilme"
937                 ],
938                 "id": "372203",
939                 "in_my_list": true,
940                 "interesting_moment": "https://art-s.nflximg.net/09544/ed4b3073394b4469fb6ec22b9df81a4f5cb09544.jpg",
941                 "list_id": "9588df32-f957-40e4-9055-1f6f33b60103_46891306",
942                 "maturity": {
943                   "board": "FSK",
944                   "description": "Nur f\u00fcr Erwachsene geeignet.",
945                   "level": 1000,
946                   "value": "18"
947                 },
948                 "quality": "540",
949                 "rating": 3.1707757,
950                 "regular_synopsis": "Ein Spielzeughersteller erweckt aus Versehen die Seele der M\u00f6rderpuppe Chucky erneut zum Leben, die sich unmittelbar wieder ihren m\u00f6rderischen Aktivit\u00e4ten zuwendet.",
951                 "runtime": 5028,
952                 "seasons_count": null,
953                 "seasons_label": null,
954                 "synopsis": "Die allseits beliebte, von D\u00e4monen besessene M\u00f6rderpuppe ist wieder da und verbreitet erneut Horror und Schrecken.",
955                 "tags": [
956                   "Brutal",
957                   "Spannend"
958                 ],
959                 "title": "Chucky 2 \u2013 Die M\u00f6rderpuppe ist wieder da",
960                 "type": "movie",
961                 "watched": false,
962                 "year": 1990
963               }
964             }
965         """
966         season_info = self.parse_season_information_for_video(video=video)
967         return {
968             id: {
969                 'id': id,
970                 'list_id': list_id,
971                 'title': video['title'],
972                 'synopsis': video['synopsis'],
973                 'regular_synopsis': video['regularSynopsis'],
974                 'type': video['summary']['type'],
975                 'rating': video['userRating']['average'],
976                 'episode_count': season_info['episode_count'],
977                 'seasons_label': season_info['seasons_label'],
978                 'seasons_count': season_info['seasons_count'],
979                 'in_my_list': video['queue']['inQueue'],
980                 'year': video['releaseYear'],
981                 'runtime': self.parse_runtime_for_video(video=video),
982                 'watched': video['watched'],
983                 'tags': self.parse_tags_for_video(video=video),
984                 'genres': self.parse_genres_for_video(video=video, genres=genres),
985                 'quality': self.parse_quality_for_video(video=video),
986                 'cast': self.parse_cast_for_video(video=video, persons=persons),
987                 'directors': self.parse_directors_for_video(video=video, persons=persons),
988                 'creators': self.parse_creators_for_video(video=video, persons=persons),
989                 'maturity': {
990                     'board': None if 'board' not in video['maturity']['rating'].keys() else video['maturity']['rating']['board'],
991                     'value': None if 'value' not in video['maturity']['rating'].keys() else video['maturity']['rating']['value'],
992                     'description': None if 'maturityDescription' not in video['maturity']['rating'].keys() else video['maturity']['rating']['maturityDescription'],
993                     'level': None if 'maturityLevel' not in video['maturity']['rating'].keys() else video['maturity']['rating']['maturityLevel']
994                 },
995                 'boxarts': {
996                     'small': video['boxarts']['_342x192']['jpg']['url'],
997                     'big': video['boxarts']['_1280x720']['jpg']['url']
998                 },
999                 'interesting_moment': None if 'interestingMoment' not in video.keys() else video['interestingMoment']['_665x375']['jpg']['url'],
1000                 'artwork': video['artWorkByType']['BILLBOARD']['_1280x720']['jpg']['url'],
1001             }
1002         }
1003
1004     def parse_creators_for_video (self, video, persons):
1005         """Matches ids with person names to generate a list of creators
1006
1007         Parameters
1008         ----------
1009         video : :obj:`dict` of :obj:`str`
1010             Dictionary entry for one video entry
1011
1012         persons : :obj:`dict` of :obj:`str`
1013             Raw resposne of all persons delivered by the API call
1014
1015         Returns
1016         -------
1017         :obj:`list` of :obj:`str`
1018             List of creators
1019         """
1020         creators = []
1021         for person_key in dict(persons).keys():
1022             if self._is_size_key(key=person_key) == False and person_key != 'summary':
1023                 for creator_key in dict(video['creators']).keys():
1024                     if self._is_size_key(key=creator_key) == False and creator_key != 'summary':
1025                         if video['creators'][creator_key][1] == person_key:
1026                             creators.append(persons[person_key]['name'])
1027         return creators
1028
1029     def parse_directors_for_video (self, video, persons):
1030         """Matches ids with person names to generate a list of directors
1031
1032         Parameters
1033         ----------
1034         video : :obj:`dict` of :obj:`str`
1035             Dictionary entry for one video entry
1036
1037         persons : :obj:`dict` of :obj:`str`
1038             Raw resposne of all persons delivered by the API call
1039
1040         Returns
1041         -------
1042         :obj:`list` of :obj:`str`
1043             List of directors
1044         """
1045         directors = []
1046         for person_key in dict(persons).keys():
1047             if self._is_size_key(key=person_key) == False and person_key != 'summary':
1048                 for director_key in dict(video['directors']).keys():
1049                     if self._is_size_key(key=director_key) == False and director_key != 'summary':
1050                         if video['directors'][director_key][1] == person_key:
1051                             directors.append(persons[person_key]['name'])
1052         return directors
1053
1054     def parse_cast_for_video (self, video, persons):
1055         """Matches ids with person names to generate a list of cast members
1056
1057         Parameters
1058         ----------
1059         video : :obj:`dict` of :obj:`str`
1060             Dictionary entry for one video entry
1061
1062         persons : :obj:`dict` of :obj:`str`
1063             Raw resposne of all persons delivered by the API call
1064
1065         Returns
1066         -------
1067         :obj:`list` of :obj:`str`
1068             List of cast members
1069         """
1070         cast = []
1071         for person_key in dict(persons).keys():
1072             if self._is_size_key(key=person_key) == False and person_key != 'summary':
1073                 for cast_key in dict(video['cast']).keys():
1074                     if self._is_size_key(key=cast_key) == False and cast_key != 'summary':
1075                         if video['cast'][cast_key][1] == person_key:
1076                             cast.append(persons[person_key]['name'])
1077         return cast
1078
1079     def parse_genres_for_video (self, video, genres):
1080         """Matches ids with genre names to generate a list of genres for a video
1081
1082         Parameters
1083         ----------
1084         video : :obj:`dict` of :obj:`str`
1085             Dictionary entry for one video entry
1086
1087         genres : :obj:`dict` of :obj:`str`
1088             Raw resposne of all genres delivered by the API call
1089
1090         Returns
1091         -------
1092         :obj:`list` of :obj:`str`
1093             List of genres
1094         """
1095         video_genres = []
1096         for genre_key in dict(genres).keys():
1097             if self._is_size_key(key=genre_key) == False and genre_key != 'summary':
1098                 for show_genre_key in dict(video['genres']).keys():
1099                     if self._is_size_key(key=show_genre_key) == False and show_genre_key != 'summary':
1100                         if video['genres'][show_genre_key][1] == genre_key:
1101                             video_genres.append(genres[genre_key]['name'])
1102         return video_genres
1103
1104     def parse_tags_for_video (self, video):
1105         """Parses a nested list of tags, removes the not needed meta information & returns a raw string list
1106
1107         Parameters
1108         ----------
1109         video : :obj:`dict` of :obj:`str`
1110             Dictionary entry for one video entry
1111
1112         Returns
1113         -------
1114         :obj:`list` of :obj:`str`
1115             List of tags
1116         """
1117         tags = []
1118         for tag_key in dict(video['tags']).keys():
1119             if self._is_size_key(key=tag_key) == False and tag_key != 'summary':
1120                 tags.append(video['tags'][tag_key]['name'])
1121         return tags
1122
1123     def parse_season_information_for_video (self, video):
1124         """Checks if the fiven video is a show (series) and returns season & episode information
1125
1126         Parameters
1127         ----------
1128         video : :obj:`dict` of :obj:`str`
1129             Dictionary entry for one video entry
1130
1131         Returns
1132         -------
1133         :obj:`dict` of :obj:`str`
1134             Episode count / Season Count & Season label if given
1135         """
1136         season_info = {
1137             'episode_count': None,
1138             'seasons_label': None,
1139             'seasons_count': None
1140         }
1141         if video['summary']['type'] == 'show':
1142             season_info = {
1143                 'episode_count': video['episodeCount'],
1144                 'seasons_label': video['numSeasonsLabel'],
1145                 'seasons_count': video['seasonCount']
1146             }
1147         return season_info
1148
1149     def parse_quality_for_video (self, video):
1150         """Transforms Netflix quality information in video resolution info
1151
1152         Parameters
1153         ----------
1154         video : :obj:`dict` of :obj:`str`
1155             Dictionary entry for one video entry
1156
1157         Returns
1158         -------
1159         :obj:`str`
1160             Quality of the video
1161         """
1162         quality = '540'
1163         if video['videoQuality']['hasHD']:
1164             quality = '720'
1165         if video['videoQuality']['hasUltraHD']:
1166             quality = '1080'
1167         return quality
1168
1169     def parse_runtime_for_video (self, video):
1170         """Checks if the video is a movie & returns the runtime if given
1171
1172         Parameters
1173         ----------
1174         video : :obj:`dict` of :obj:`str`
1175             Dictionary entry for one video entry
1176
1177         Returns
1178         -------
1179         :obj:`str`
1180             Runtime of the video (in seconds)
1181         """
1182         runtime = None
1183         if video['summary']['type'] != 'show':
1184             runtime = video['runtime']
1185         return runtime
1186
1187     def parse_netflix_list_id (self, video_list):
1188         """Parse a video list and extract the list id
1189
1190         Parameters
1191         ----------
1192         video_list : :obj:`dict` of :obj:`str`
1193             Netflix video list
1194
1195         Returns
1196         -------
1197         entry : :obj:`str` or None
1198             Netflix list id
1199         """
1200         netflix_list_id = None
1201         if 'lists' in video_list.keys():
1202             for video_id in video_list['lists']:
1203                 if self._is_size_key(key=video_id) == False:
1204                     netflix_list_id = video_id;
1205         return netflix_list_id
1206
1207     def parse_show_information (self, id, response_data):
1208         """Parse extended show information (synopsis, seasons, etc.)
1209
1210         Parameters
1211         ----------
1212         id : :obj:`str`
1213             Video id
1214
1215         response_data : :obj:`dict` of :obj:`str`
1216             Parsed response JSON from the `fetch_show_information` call
1217
1218         Returns
1219         -------
1220         entry : :obj:`dict` of :obj:`str`
1221         Show information in the format:
1222             {
1223                 "season_id": "80113084",
1224                 "synopsis": "Aus verzweifelter Geldnot versucht sich der Familienvater und Drucker Jochen als Geldf\u00e4lscher und rutscht dabei immer mehr in die dunkle Welt des Verbrechens ab."
1225                 "detail_text": "I´m optional"
1226             }
1227         """
1228         show = {}
1229         raw_show = response_data['value']['videos'][id]
1230         show.update({'synopsis': raw_show['regularSynopsis']})
1231         if 'evidence' in raw_show:
1232             show.update({'detail_text': raw_show['evidence']['value']['text']})
1233         if 'seasonList' in raw_show:
1234             show.update({'season_id': raw_show['seasonList']['current'][1]})
1235         return show
1236
1237     def parse_seasons (self, id, response_data):
1238         """Parse a list of seasons for a given show
1239
1240         Parameters
1241         ----------
1242         id : :obj:`str`
1243             Season id
1244
1245         response_data : :obj:`dict` of :obj:`str`
1246             Parsed response JSON from the `fetch_seasons_for_show` call
1247
1248         Returns
1249         -------
1250         entry : :obj:`dict` of :obj:`dict` of :obj:`str`
1251         Season information in the format:
1252             {
1253                 "80113084": {
1254                     "id": 80113084,
1255                     "text": "Season 1",
1256                     "shortName": "St. 1",
1257                     "boxarts": {
1258                       "big": "https://art-s.nflximg.net/5e7d3/b3b48749843fd3a36db11c319ffa60f96b55e7d3.jpg",
1259                       "small": "https://art-s.nflximg.net/57543/a039845c2eb9186dc26019576d895bf5a1957543.jpg"
1260                     },
1261                     "interesting_moment": "https://art-s.nflximg.net/09544/ed4b3073394b4469fb6ec22b9df81a4f5cb09544.jpg"
1262                 },
1263                 "80113085": {
1264                     "id": 80113085,
1265                     "text": "Season 2",
1266                     "shortName": "St. 2",
1267                     "boxarts": {
1268                       "big": "https://art-s.nflximg.net/5e7d3/b3b48749843fd3a36db11c319ffa60f96b55e7d3.jpg",
1269                       "small": "https://art-s.nflximg.net/57543/a039845c2eb9186dc26019576d895bf5a1957543.jpg"
1270                     },
1271                     "interesting_moment": "https://art-s.nflximg.net/09544/ed4b3073394b4469fb6ec22b9df81a4f5cb09544.jpg"
1272                 }
1273             }
1274         """
1275         seasons = {}
1276         raw_seasons = response_data['value']
1277         for season in raw_seasons['seasons']:
1278             if self._is_size_key(key=season) == False:
1279                 seasons.update(self.parse_season_entry(season=raw_seasons['seasons'][season], videos=raw_seasons['videos']))
1280         return seasons
1281
1282     def parse_season_entry (self, season, videos):
1283         """Parse a season list entry e.g. rip out the parts we need
1284
1285         Parameters
1286         ----------
1287         season : :obj:`dict` of :obj:`str`
1288             Season entry from the `fetch_seasons_for_show` call
1289
1290         Returns
1291         -------
1292         entry : :obj:`dict` of :obj:`dict` of :obj:`str`
1293             Season list entry in the format:
1294
1295             {
1296                 "80113084": {
1297                     "id": 80113084,
1298                     "text": "Season 1",
1299                     "shortName": "St. 1",
1300                     "boxarts": {
1301                       "big": "https://art-s.nflximg.net/5e7d3/b3b48749843fd3a36db11c319ffa60f96b55e7d3.jpg",
1302                       "small": "https://art-s.nflximg.net/57543/a039845c2eb9186dc26019576d895bf5a1957543.jpg"
1303                     },
1304                     "interesting_moment": "https://art-s.nflximg.net/09544/ed4b3073394b4469fb6ec22b9df81a4f5cb09544.jpg"
1305                 }
1306             }
1307         """
1308         # get art video key
1309         video_key = ''
1310         for key in videos.keys():
1311             if self._is_size_key(key=key) == False:
1312                 video_key = key
1313         return {
1314             season['summary']['id']: {
1315                 'id': season['summary']['id'],
1316                 'text': season['summary']['name'],
1317                 'shortName': season['summary']['shortName'],
1318                 'boxarts': {
1319                     'small': videos[video_key]['boxarts']['_342x192']['jpg']['url'],
1320                     'big': videos[video_key]['boxarts']['_1280x720']['jpg']['url']
1321                 },
1322                 'interesting_moment': videos[video_key]['interestingMoment']['_665x375']['jpg']['url'],
1323             }
1324         }
1325
1326     def parse_episodes_by_season (self, response_data):
1327         """Parse episodes for a given season/episode list
1328
1329         Parameters
1330         ----------
1331         response_data : :obj:`dict` of :obj:`str`
1332             Parsed response JSON from the `fetch_seasons_for_show` call
1333
1334         Returns
1335         -------
1336         entry : :obj:`dict` of :obj:`dict` of :obj:`str`
1337         Season information in the format:
1338
1339         {
1340           "70251729": {
1341             "banner": "https://art-s.nflximg.net/63a36/c7fdfe6604ef2c22d085ac5dca5f69874e363a36.jpg",
1342             "duration": 1387,
1343             "episode": 1,
1344             "fanart": "https://art-s.nflximg.net/74e02/e7edcc5cc7dcda1e94d505df2f0a2f0d22774e02.jpg",
1345             "genres": [
1346               "Serien",
1347               "Comedyserien"
1348             ],
1349             "id": 70251729,
1350             "mediatype": "episode",
1351             "mpaa": "FSK 16",
1352             "my_list": false,
1353             "playcount": 0,
1354             "plot": "Als die Griffins und andere Einwohner von Quahog in die Villa von James Woods eingeladen werden, muss pl\u00f6tzlich ein Mord aufgekl\u00e4rt werden.",
1355             "poster": "https://art-s.nflximg.net/72fd6/57088715e8d436fdb6986834ab39124b0a972fd6.jpg",
1356             "rating": 3.9111512,
1357             "season": 9,
1358             "thumb": "https://art-s.nflximg.net/be686/07680670a68da8749eba607efb1ae37f9e3be686.jpg",
1359             "title": "Und dann gab es weniger (Teil 1)",
1360             "year": 2010,
1361             "bookmark": -1
1362           },
1363           "70251730": {
1364             "banner": "https://art-s.nflximg.net/63a36/c7fdfe6604ef2c22d085ac5dca5f69874e363a36.jpg",
1365             "duration": 1379,
1366             "episode": 2,
1367             "fanart": "https://art-s.nflximg.net/c472c/6c10f9578bf2c1d0a183c2ccb382931efcbc472c.jpg",
1368             "genres": [
1369               "Serien",
1370               "Comedyserien"
1371             ],
1372             "id": 70251730,
1373             "mediatype": "episode",
1374             "mpaa": "FSK 16",
1375             "my_list": false,
1376             "playcount": 1,
1377             "plot": "Wer ist der M\u00f6rder? Nach zahlreichen Morden wird immer wieder jemand anderes verd\u00e4chtigt.",
1378             "poster": "https://art-s.nflximg.net/72fd6/57088715e8d436fdb6986834ab39124b0a972fd6.jpg",
1379             "rating": 3.9111512,
1380             "season": 9,
1381             "thumb": "https://art-s.nflximg.net/15a08/857d59126641987bec302bb147a802a00d015a08.jpg",
1382             "title": "Und dann gab es weniger (Teil 2)",
1383             "year": 2010,
1384             "bookmark": 1234
1385           },
1386         }
1387         """
1388         episodes = {}
1389         raw_episodes = response_data['value']['videos']
1390         for episode_id in raw_episodes:
1391             if self._is_size_key(key=episode_id) == False:
1392                 if (raw_episodes[episode_id]['summary']['type'] == 'episode'):
1393                     episodes.update(self.parse_episode(episode=raw_episodes[episode_id], genres=response_data['value']['genres']))
1394         return episodes
1395
1396     def parse_episode (self, episode, genres=None):
1397         """Parse episode from an list of episodes by season
1398
1399         Parameters
1400         ----------
1401         episode : :obj:`dict` of :obj:`str`
1402             Episode entry from the `fetch_episodes_by_season` call
1403
1404         Returns
1405         -------
1406         entry : :obj:`dict` of :obj:`dict` of :obj:`str`
1407         Episode information in the format:
1408
1409         {
1410           "70251729": {
1411             "banner": "https://art-s.nflximg.net/63a36/c7fdfe6604ef2c22d085ac5dca5f69874e363a36.jpg",
1412             "duration": 1387,
1413             "episode": 1,
1414             "fanart": "https://art-s.nflximg.net/74e02/e7edcc5cc7dcda1e94d505df2f0a2f0d22774e02.jpg",
1415             "genres": [
1416               "Serien",
1417               "Comedyserien"
1418             ],
1419             "id": 70251729,
1420             "mediatype": "episode",
1421             "mpaa": "FSK 16",
1422             "my_list": false,
1423             "playcount": 0,
1424             "plot": "Als die Griffins und andere Einwohner von Quahog in die Villa von James Woods eingeladen werden, muss pl\u00f6tzlich ein Mord aufgekl\u00e4rt werden.",
1425             "poster": "https://art-s.nflximg.net/72fd6/57088715e8d436fdb6986834ab39124b0a972fd6.jpg",
1426             "rating": 3.9111512,
1427             "season": 9,
1428             "thumb": "https://art-s.nflximg.net/be686/07680670a68da8749eba607efb1ae37f9e3be686.jpg",
1429             "title": "Und dann gab es weniger (Teil 1)",
1430             "year": 2010,
1431             "bookmark": 1234
1432           },
1433         }
1434         """
1435         return {
1436             episode['summary']['id']: {
1437                 'id': episode['summary']['id'],
1438                 'episode': episode['summary']['episode'],
1439                 'season': episode['summary']['season'],
1440                 'plot': episode['info']['synopsis'],
1441                 'duration': episode['info']['runtime'],
1442                 'title': episode['info']['title'],
1443                 'year': episode['info']['releaseYear'],
1444                 'genres': self.parse_genres_for_video(video=episode, genres=genres),
1445                 'mpaa': str(episode['maturity']['rating']['board']) + ' ' + str(episode['maturity']['rating']['value']),
1446                 'maturity': episode['maturity'],
1447                 'playcount': (0, 1)[episode['watched']],
1448                 'rating': episode['userRating']['average'],
1449                 'thumb': episode['info']['interestingMoments']['url'],
1450                 'fanart': episode['interestingMoment']['_1280x720']['jpg']['url'],
1451                 'poster': episode['boxarts']['_1280x720']['jpg']['url'],
1452                 'banner': episode['boxarts']['_342x192']['jpg']['url'],
1453                 'mediatype': {'episode': 'episode', 'movie': 'movie'}[episode['summary']['type']],
1454                 'my_list': episode['queue']['inQueue'],
1455                 'bookmark': episode['bookmarkPosition']
1456             }
1457         }
1458
1459     def fetch_browse_list_contents (self):
1460         """Fetches the HTML data for the lists on the landing page (browse page) of Netflix
1461
1462         Returns
1463         -------
1464         :obj:`BeautifulSoup`
1465             Instance of an BeautifulSoup document containing the complete page contents
1466         """
1467         response = self.session.get(self._get_document_url_for(component='browse'), verify=self.verify_ssl)
1468         return BeautifulSoup(response.text)
1469
1470     def fetch_video_list_ids (self, list_from=0, list_to=50):
1471         """Fetches the JSON with detailed information based on the lists on the landing page (browse page) of Netflix
1472
1473         Parameters
1474         ----------
1475         list_from : :obj:`int`
1476             Start entry for pagination
1477
1478         list_to : :obj:`int`
1479             Last entry for pagination
1480
1481         Returns
1482         -------
1483         :obj:`dict` of :obj:`dict` of :obj:`str`
1484             Raw Netflix API call response or api call error
1485         """
1486         payload = {
1487             'fromRow': list_from,
1488             'toRow': list_to,
1489             'opaqueImageExtension': 'jpg',
1490             'transparentImageExtension': 'png',
1491             '_': int(time.time()),
1492             'authURL': self.user_data['authURL']
1493         }
1494         url = self._get_api_url_for(component='video_list_ids')
1495         response = self.session.get(url, params=payload, verify=self.verify_ssl);
1496         return self._process_response(response=response, component=url)
1497
1498     def fetch_search_results (self, search_str, list_from=0, list_to=48):
1499         """Fetches the JSON which contains the results for the given search query
1500
1501         Parameters
1502         ----------
1503         search_str : :obj:`str`
1504             String to query Netflix search for
1505
1506         list_from : :obj:`int`
1507             Start entry for pagination
1508
1509         list_to : :obj:`int`
1510             Last entry for pagination
1511
1512         Returns
1513         -------
1514         :obj:`dict` of :obj:`dict` of :obj:`str`
1515             Raw Netflix API call response or api call error
1516         """
1517         # properly encode the search string
1518         encoded_search_string = urllib.quote(search_str)
1519
1520         paths = [
1521             ['search', encoded_search_string, 'titles', {'from': list_from, 'to': list_to}, ['summary', 'title']],
1522             ['search', encoded_search_string, 'titles', {'from': list_from, 'to': list_to}, 'boxarts', '_342x192', 'jpg'],
1523             ['search', encoded_search_string, 'titles', ['id', 'length', 'name', 'trackIds', 'requestId']]
1524         ]
1525         response = self._path_request(paths=paths)
1526         return self._process_response(response=response, component='Search results')
1527
1528     def fetch_video_list (self, list_id, list_from=0, list_to=20):
1529         """Fetches the JSON which contains the contents of a given video list
1530
1531         Parameters
1532         ----------
1533         list_id : :obj:`str`
1534             Unique list id to query Netflix for
1535
1536         list_from : :obj:`int`
1537             Start entry for pagination
1538
1539         list_to : :obj:`int`
1540             Last entry for pagination
1541
1542         Returns
1543         -------
1544         :obj:`dict` of :obj:`dict` of :obj:`str`
1545             Raw Netflix API call response or api call error
1546         """
1547         paths = [
1548             ['lists', list_id, {'from': list_from, 'to': list_to}, ['summary', 'title', 'synopsis', 'regularSynopsis', 'evidence', 'queue', 'episodeCount', 'info', 'maturity', 'runtime', 'seasonCount', 'releaseYear', 'userRating', 'numSeasonsLabel', 'bookmarkPosition', 'watched', 'videoQuality']],
1549             ['lists', list_id, {'from': list_from, 'to': list_to}, 'cast', {'from': 0, 'to': 15}, ['id', 'name']],
1550             ['lists', list_id, {'from': list_from, 'to': list_to}, 'cast', 'summary'],
1551             ['lists', list_id, {'from': list_from, 'to': list_to}, 'genres', {'from': 0, 'to': 5}, ['id', 'name']],
1552             ['lists', list_id, {'from': list_from, 'to': list_to}, 'genres', 'summary'],
1553             ['lists', list_id, {'from': list_from, 'to': list_to}, 'tags', {'from': 0, 'to': 9}, ['id', 'name']],
1554             ['lists', list_id, {'from': list_from, 'to': list_to}, 'tags', 'summary'],
1555             ['lists', list_id, {'from': list_from, 'to': list_to}, ['creators', 'directors'], {'from': 0, 'to': 49}, ['id', 'name']],
1556             ['lists', list_id, {'from': list_from, 'to': list_to}, ['creators', 'directors'], 'summary'],
1557             ['lists', list_id, {'from': list_from, 'to': list_to}, 'bb2OGLogo', '_400x90', 'png'],
1558             ['lists', list_id, {'from': list_from, 'to': list_to}, 'boxarts', '_342x192', 'jpg'],
1559             ['lists', list_id, {'from': list_from, 'to': list_to}, 'boxarts', '_1280x720', 'jpg'],
1560             ['lists', list_id, {'from': list_from, 'to': list_to}, 'storyarts', '_1632x873', 'jpg'],
1561             ['lists', list_id, {'from': list_from, 'to': list_to}, 'interestingMoment', '_665x375', 'jpg'],
1562             ['lists', list_id, {'from': list_from, 'to': list_to}, 'artWorkByType', 'BILLBOARD', '_1280x720', 'jpg']
1563         ];
1564
1565         response = self._path_request(paths=paths)
1566         return self._process_response(response=response, component='Video list')
1567
1568     def fetch_video_list_information (self, video_ids):
1569         """Fetches the JSON which contains the detail information of a list of given video ids
1570
1571         Parameters
1572         ----------
1573         video_ids : :obj:`list` of :obj:`str`
1574             List of video ids to fetch detail data for
1575
1576         Returns
1577         -------
1578         :obj:`dict` of :obj:`dict` of :obj:`str`
1579             Raw Netflix API call response or api call error
1580         """
1581         paths = []
1582         for video_id in video_ids:
1583             paths.append(['videos', video_id, ['summary', 'title', 'synopsis', 'regularSynopsis', 'evidence', 'queue', 'episodeCount', 'info', 'maturity', 'runtime', 'seasonCount', 'releaseYear', 'userRating', 'numSeasonsLabel', 'bookmarkPosition', 'watched', 'videoQuality']])
1584             paths.append(['videos', video_id, 'cast', {'from': 0, 'to': 15}, ['id', 'name']])
1585             paths.append(['videos', video_id, 'cast', 'summary'])
1586             paths.append(['videos', video_id, 'genres', {'from': 0, 'to': 5}, ['id', 'name']])
1587             paths.append(['videos', video_id, 'genres', 'summary'])
1588             paths.append(['videos', video_id, 'tags', {'from': 0, 'to': 9}, ['id', 'name']])
1589             paths.append(['videos', video_id, 'tags', 'summary'])
1590             paths.append(['videos', video_id, ['creators', 'directors'], {'from': 0, 'to': 49}, ['id', 'name']])
1591             paths.append(['videos', video_id, ['creators', 'directors'], 'summary'])
1592             paths.append(['videos', video_id, 'bb2OGLogo', '_400x90', 'png'])
1593             paths.append(['videos', video_id, 'boxarts', '_342x192', 'jpg'])
1594             paths.append(['videos', video_id, 'boxarts', '_1280x720', 'jpg'])
1595             paths.append(['videos', video_id, 'storyarts', '_1632x873', 'jpg'])
1596             paths.append(['videos', video_id, 'interestingMoment', '_665x375', 'jpg'])
1597             paths.append(['videos', video_id, 'artWorkByType', 'BILLBOARD', '_1280x720', 'jpg'])
1598
1599         response = self._path_request(paths=paths)
1600         return self._process_response(response=response, component='fetch_video_list_information')
1601
1602     def fetch_metadata (self, id):
1603         """Fetches the JSON which contains the metadata for a given show/movie or season id
1604
1605         Parameters
1606         ----------
1607         id : :obj:`str`
1608             Show id, movie id or season id
1609
1610         Returns
1611         -------
1612         :obj:`dict` of :obj:`dict` of :obj:`str`
1613             Raw Netflix API call response or api call error
1614         """
1615         payload = {
1616             'movieid': id,
1617             'imageformat': 'jpg',
1618             '_': int(time.time())
1619         }
1620         url = self._get_api_url_for(component='metadata')
1621         response = self.session.get(url, params=payload, verify=self.verify_ssl);
1622         return self._process_response(response=response, component=url)
1623
1624     def fetch_show_information (self, id, type):
1625         """Fetches the JSON which contains the detailed contents of a show
1626
1627         Parameters
1628         ----------
1629         id : :obj:`str`
1630             Unique show id to query Netflix for
1631
1632         type : :obj:`str`
1633             Can be 'movie' or 'show'
1634
1635         Returns
1636         -------
1637         :obj:`dict` of :obj:`dict` of :obj:`str`
1638             Raw Netflix API call response or api call error
1639         """
1640         # check if we have a show or a movie, the request made depends on this
1641         if type == 'show':
1642             paths = [
1643                 ['videos', id, ['requestId', 'regularSynopsis', 'evidence']],
1644                 ['videos', id, 'seasonList', 'current', 'summary']
1645             ]
1646         else:
1647             paths = [['videos', id, ['requestId', 'regularSynopsis', 'evidence']]]
1648         response = self._path_request(paths=paths)
1649         return self._process_response(response=response, component='Show information')
1650
1651     def fetch_seasons_for_show (self, id, list_from=0, list_to=30):
1652         """Fetches the JSON which contains the seasons of a given show
1653
1654         Parameters
1655         ----------
1656         id : :obj:`str`
1657             Unique show id to query Netflix for
1658
1659         list_from : :obj:`int`
1660             Start entry for pagination
1661
1662         list_to : :obj:`int`
1663             Last entry for pagination
1664
1665         Returns
1666         -------
1667         :obj:`dict` of :obj:`dict` of :obj:`str`
1668             Raw Netflix API call response or api call error
1669         """
1670         paths = [
1671             ['videos', id, 'seasonList', {'from': list_from, 'to': list_to}, 'summary'],
1672             ['videos', id, 'seasonList', 'summary'],
1673             ['videos', id, 'boxarts',  '_342x192', 'jpg'],
1674             ['videos', id, 'boxarts', '_1280x720', 'jpg'],
1675             ['videos', id, 'storyarts',  '_1632x873', 'jpg'],
1676             ['videos', id, 'interestingMoment', '_665x375', 'jpg']
1677         ]
1678         response = self._path_request(paths=paths)
1679         return self._process_response(response=response, component='Seasons')
1680
1681     def fetch_episodes_by_season (self, season_id, list_from=-1, list_to=40):
1682         """Fetches the JSON which contains the episodes of a given season
1683
1684         TODO: Add more metadata
1685
1686         Parameters
1687         ----------
1688         season_id : :obj:`str`
1689             Unique season_id id to query Netflix for
1690
1691         list_from : :obj:`int`
1692             Start entry for pagination
1693
1694         list_to : :obj:`int`
1695             Last entry for pagination
1696
1697         Returns
1698         -------
1699         :obj:`dict` of :obj:`dict` of :obj:`str`
1700             Raw Netflix API call response or api call error
1701         """
1702         paths = [
1703             ['seasons', season_id, 'episodes', {'from': list_from, 'to': list_to}, ['summary', 'queue', 'info', 'maturity', 'userRating', 'bookmarkPosition', 'creditOffset', 'watched', 'videoQuality']],
1704             #['videos', season_id, 'cast', {'from': 0, 'to': 15}, ['id', 'name']],
1705             #['videos', season_id, 'cast', 'summary'],
1706             #['videos', season_id, 'genres', {'from': 0, 'to': 5}, ['id', 'name']],
1707             #['videos', season_id, 'genres', 'summary'],
1708             #['videos', season_id, 'tags', {'from': 0, 'to': 9}, ['id', 'name']],
1709             #['videos', season_id, 'tags', 'summary'],
1710             #['videos', season_id, ['creators', 'directors'], {'from': 0, 'to': 49}, ['id', 'name']],
1711             #['videos', season_id, ['creators', 'directors'], 'summary'],
1712             ['seasons', season_id, 'episodes', {'from': list_from, 'to': list_to}, 'genres', {'from': 0, 'to': 1}, ['id', 'name']],
1713             ['seasons', season_id, 'episodes', {'from': list_from, 'to': list_to}, 'genres', 'summary'],
1714             ['seasons', season_id, 'episodes', {'from': list_from, 'to': list_to}, 'interestingMoment', '_1280x720', 'jpg'],
1715             ['seasons', season_id, 'episodes', {'from': list_from, 'to': list_to}, 'interestingMoment', '_665x375', 'jpg'],
1716             ['seasons', season_id, 'episodes', {'from': list_from, 'to': list_to}, 'boxarts', '_342x192', 'jpg'],
1717             ['seasons', season_id, 'episodes', {'from': list_from, 'to': list_to}, 'boxarts', '_1280x720', 'jpg']
1718         ]
1719         response = self._path_request(paths=paths)
1720         return self._process_response(response=response, component='fetch_episodes_by_season')
1721
1722     def refresh_session_data (self, account):
1723         """Reload the session data (profiles, user_data, api_data)
1724
1725         Parameters
1726         ----------
1727         account : :obj:`dict` of :obj:`str`
1728             Dict containing an email, country & a password property
1729         """
1730         # load the profiles page (to verify the user)
1731         response = self.session.get(self._get_document_url_for(component='profiles'), verify=self.verify_ssl)
1732         # parse out the needed inline information
1733         page_soup = BeautifulSoup(response.text)
1734         page_data = self.extract_inline_netflix_page_data(page_soup=page_soup)
1735         self._parse_page_contents(page_soup)
1736         account_hash = self._generate_account_hash(account=account)
1737         self._save_data(filename=self.data_path + '_' + account_hash)
1738
1739     def _path_request (self, paths):
1740         """Executes a post request against the shakti endpoint with Falcor style payload
1741
1742         Parameters
1743         ----------
1744         paths : :obj:`list` of :obj:`list`
1745             Payload with path querys for the Netflix Shakti API in Falcor style
1746
1747         Returns
1748         -------
1749         :obj:`requests.response`
1750             Response from a POST call made with Requests
1751         """
1752         headers = {
1753             'Content-Type': 'application/json',
1754             'Accept': 'application/json, text/javascript, */*',
1755         }
1756
1757         data = json.dumps({
1758             'paths': paths,
1759             'authURL': self.user_data['authURL']
1760         })
1761
1762         params = {
1763             'withSize': True,
1764             'materialize': True,
1765             'model': self.user_data['gpsModel']
1766         }
1767
1768         return self.session.post(self._get_api_url_for(component='shakti'), params=params, headers=headers, data=data, verify=self.verify_ssl)
1769
1770     def _is_size_key (self, key):
1771         """Tiny helper that checks if a given key is called $size or size, as we need to check this often
1772
1773         Parameters
1774         ----------
1775         key : :obj:`str`
1776             Key to check the value for
1777
1778         Returns
1779         -------
1780         bool
1781             Key has a size value or not
1782         """
1783         return key == '$size' or key == 'size'
1784
1785     def _get_api_url_for (self, component):
1786         """Tiny helper that builds the url for a requested API endpoint component
1787
1788         Parameters
1789         ----------
1790         component : :obj:`str`
1791             Component endpoint to build the URL for
1792
1793         Returns
1794         -------
1795         :obj:`str`
1796             API Url
1797         """
1798         return self.api_data['API_ROOT'] + self.api_data['API_BASE_URL'] + '/' + self.api_data['BUILD_IDENTIFIER'] + self.urls[component]
1799
1800     def _get_document_url_for (self, component):
1801         """Tiny helper that builds the url for a requested document endpoint component
1802
1803         Parameters
1804         ----------
1805         component : :obj:`str`
1806             Component endpoint to build the URL for
1807
1808         Returns
1809         -------
1810         :obj:`str`
1811             Document Url
1812         """
1813         return self.base_url + self.urls[component]
1814
1815     def _process_response (self, response, component):
1816         """Tiny helper to check responses for API requests
1817
1818         Parameters
1819         ----------
1820         response : :obj:`requests.response`
1821             Response from a requests instance
1822
1823         component : :obj:`str`
1824             Component endpoint
1825
1826         Returns
1827         -------
1828         :obj:`dict` of :obj:`dict` of :obj:`str` or :obj:`dict` of :obj:`str`
1829             Raw Netflix API call response or api call error
1830         """
1831         # check if we´re not authorized to make thios call
1832         if response.status_code == 401:
1833             return {
1834                 'error': True,
1835                 'message': 'Session invalid',
1836                 'code': 401
1837             }
1838         # check if somethign else failed
1839         if response.status_code != 200:
1840             return {
1841                 'error': True,
1842                 'message': 'API call for "' + component + '" failed',
1843                 'code': response.status_code
1844             }
1845         # return the parsed response & everything´s fine
1846         return response.json()
1847
1848     def _update_my_list (self, video_id, operation):
1849         """Tiny helper to add & remove items from "my list"
1850
1851         Parameters
1852         ----------
1853         video_id : :obj:`str`
1854             ID of the show/movie to be added
1855
1856         operation : :obj:`str`
1857             Either "add" or "remove"
1858
1859         Returns
1860         -------
1861         bool
1862             Operation successfull
1863         """
1864         headers = {
1865             'Content-Type': 'application/json',
1866             'Accept': 'application/json, text/javascript, */*',
1867         }
1868
1869         payload = json.dumps({
1870             'operation': operation,
1871             'videoId': int(video_id),
1872             'authURL': self.user_data['authURL']
1873         })
1874
1875         response = self.session.post(self._get_api_url_for(component='update_my_list'), headers=headers, data=payload, verify=self.verify_ssl)
1876         return response.status_code == 200
1877
1878     def _save_data(self, filename):
1879         """Tiny helper that stores session data from the session in a given file
1880
1881         Parameters
1882         ----------
1883         filename : :obj:`str`
1884             Complete path incl. filename that determines where to store the cookie
1885
1886         Returns
1887         -------
1888         bool
1889             Storage procedure was successfull
1890         """
1891         if not os.path.isdir(os.path.dirname(filename)):
1892             return False
1893         with open(filename, 'w') as f:
1894             f.truncate()
1895             pickle.dump({
1896                 'user_data': self.user_data,
1897                 'api_data': self.api_data,
1898                 'profiles': self.profiles
1899             }, f)
1900
1901     def _load_data(self, filename):
1902         """Tiny helper that loads session data into the active session from a given file
1903
1904         Parameters
1905         ----------
1906         filename : :obj:`str`
1907             Complete path incl. filename that determines where to load the data from
1908
1909         Returns
1910         -------
1911         bool
1912             Load procedure was successfull
1913         """
1914         if not os.path.isfile(filename):
1915             return False
1916
1917         with open(filename) as f:
1918             data = pickle.load(f)
1919             if data:
1920                 self.profiles = data['profiles']
1921                 self.user_data = data['user_data']
1922                 self.api_data = data['api_data']
1923             else:
1924                 return False
1925
1926     def _delete_data (self, path):
1927         """Tiny helper that deletes session data
1928
1929         Parameters
1930         ----------
1931         filename : :obj:`str`
1932             Complete path incl. filename that determines where to delete the files
1933
1934         """
1935         head, tail = os.path.split(path)
1936         for subdir, dirs, files in os.walk(head):
1937             for file in files:
1938                 if tail in file:
1939                     os.remove(os.path.join(subdir, file))
1940
1941     def _save_cookies(self, filename):
1942         """Tiny helper that stores cookies from the session in a given file
1943
1944         Parameters
1945         ----------
1946         filename : :obj:`str`
1947             Complete path incl. filename that determines where to store the cookie
1948
1949         Returns
1950         -------
1951         bool
1952             Storage procedure was successfull
1953         """
1954         if not os.path.isdir(os.path.dirname(filename)):
1955             return False
1956         with open(filename, 'w') as f:
1957             f.truncate()
1958             pickle.dump(self.session.cookies._cookies, f)
1959
1960     def _load_cookies(self, filename):
1961         """Tiny helper that loads cookies into the active session from a given file
1962
1963         Parameters
1964         ----------
1965         filename : :obj:`str`
1966             Complete path incl. filename that determines where to load the cookie from
1967
1968         Returns
1969         -------
1970         bool
1971             Load procedure was successfull
1972         """
1973         if not os.path.isfile(filename):
1974             return False
1975
1976         with open(filename) as f:
1977             cookies = pickle.load(f)
1978             if cookies:
1979                 jar = requests.cookies.RequestsCookieJar()
1980                 jar._cookies = cookies
1981                 self.session.cookies = jar
1982             else:
1983                 return False
1984
1985     def _delete_cookies (self, path):
1986         """Tiny helper that deletes cookie data
1987
1988         Parameters
1989         ----------
1990         filename : :obj:`str`
1991             Complete path incl. filename that determines where to delete the files
1992
1993         """
1994         head, tail = os.path.split(path)
1995         for subdir, dirs, files in os.walk(head):
1996             for file in files:
1997                 if tail in file:
1998                     os.remove(os.path.join(subdir, file))
1999
2000     def _generate_account_hash (self, account):
2001         """Generates a has for the given account (used for cookie verification)
2002
2003         Parameters
2004         ----------
2005         account : :obj:`dict` of :obj:`str`
2006             Dict containing an email, country & a password property
2007
2008         Returns
2009         -------
2010         :obj:`str`
2011             Account data hash
2012         """
2013         return base64.urlsafe_b64encode(account['email'])