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