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