11# coding: utf-8
22from __future__ import print_function , unicode_literals
33
4+ import logging
45from uuid import uuid4
6+ from datetime import datetime
57from operator import itemgetter
68try : # python2
79 from urlparse import urljoin
1113
1214import requests
1315
14- from .consts import BASE_URL , LOGIN_URL
16+ from .consts import BASE_URL
1517from .exception import NotLoginError , APIError
1618from .article import Article
1719from .subscription import Subscription
1820
1921
22+ LOGGER = logging .getLogger (__name__ )
23+
24+
2025class InoreaderClient (object ):
2126
22- def __init__ (self , app_id , app_key , userid = None , auth_token = None ):
27+ # paths
28+ TOKEN_PATH = '/oauth2/token'
29+ USER_INFO_PATH = 'user-info'
30+ TAG_LIST_PATH = 'tag/list'
31+ SUBSCRIPTION_LIST_PATH = 'subscription/list'
32+ STREAM_CONTENTS_PATH = 'stream/contents/'
33+ EDIT_TAG_PATH = 'edit-tag'
34+
35+ # tags
36+ GENERAL_TAG_TEMPLATE = 'user/-/label/{}'
37+ READ_TAG = 'user/-/state/com.google/read'
38+ STARRED_TAG = 'user/-/state/com.google/starred'
39+ LIKED_TAG = 'user/-/state/com.google/like'
40+ BROADCAST_TAG = 'user/-/state/com.google/broadcast'
41+
42+ def __init__ (self , app_id , app_key , access_token , refresh_token ,
43+ expires_at , config_manager = None ):
2344 self .app_id = app_id
2445 self .app_key = app_key
25- self .auth_token = auth_token
46+ self .access_token = access_token
47+ self .refresh_token = refresh_token
48+ self .expires_at = float (expires_at )
2649 self .session = requests .Session ()
2750 self .session .headers .update ({
2851 'AppId' : self .app_id ,
2952 'AppKey' : self .app_key ,
30- 'Authorization' : 'GoogleLogin auth= {}' .format (self .auth_token )
53+ 'Authorization' : 'Bearer {}' .format (self .access_token )
3154 })
32- if userid :
33- self .userid = userid
34- else :
35- self .userid = None if not self .auth_token else self .userinfo ()['userId' ]
36-
37- def userinfo (self ):
38- if not self .auth_token :
39- raise NotLoginError
55+ self .config_manager = config_manager
4056
41- url = urljoin ( BASE_URL , 'user-info' )
42- resp = self . session . post ( url )
43- if resp . status_code != 200 :
44- raise APIError ( resp . text )
57+ def check_token ( self ):
58+ now = datetime . now (). timestamp ( )
59+ if now >= self . expires_at :
60+ self . refresh_access_token ( )
4561
46- return resp .json ()
47-
48- def login (self , username , password ):
49- resp = self .session .get (LOGIN_URL , params = {'Email' : username , 'Passwd' : password })
50- if resp .status_code != 200 :
51- return False
62+ @staticmethod
63+ def parse_response (response , json_data = True ):
64+ if response .status_code == 401 :
65+ raise NotLoginError
66+ elif response .status_code != 200 :
67+ raise APIError (response .text )
68+
69+ return response .json () if json_data else response .text
70+
71+ def refresh_access_token (self ):
72+ url = urljoin (BASE_URL , self .TOKEN_PATH )
73+ payload = {
74+ 'client_id' : self .app_id ,
75+ 'client_secret' : self .app_key ,
76+ 'grant_type' : 'refresh_token' ,
77+ 'refresh_token' : self .refresh_token ,
78+ }
79+ response = self .parse_response (requests .post (url , json = payload ))
80+ self .access_token = response ['access_token' ]
81+ self .refresh_token = response ['refresh_token' ]
82+ self .expires_at = datetime .now ().timestamp () + response ['expires_in' ]
83+ self .session .headers ['Authorization' ] = 'Bear {}' .format (self .access_token )
84+
85+ if self .config_manager :
86+ self .config_manager .access_token = self .access_token
87+ self .config_manager .refresh_token = self .refresh_token
88+ self .config_manager .expires_at = self .expires_at
89+ self .config_manager .save ()
5290
53- for line in resp .text .split ('\n ' ):
54- if line .startswith ('Auth' ):
55- self .auth_token = line .replace ('Auth=' , '' ).strip ()
91+ def userinfo (self ):
92+ self .check_token ()
5693
57- return bool (self .auth_token )
94+ url = urljoin (BASE_URL , self .USER_INFO_PATH )
95+ return self .parse_response (self .session .post (url ))
5896
5997 def get_folders (self ):
60- if not self .auth_token :
61- raise NotLoginError
98+ self .check_token ()
6299
63- url = urljoin (BASE_URL , 'tag/list' )
100+ url = urljoin (BASE_URL , self . TAG_LIST_PATH )
64101 params = {'types' : 1 , 'counts' : 1 }
65- resp = self .session .post (url , params = params )
66- if resp .status_code != 200 :
67- raise APIError (resp .text )
102+ response = self .parse_response (self .session .post (url , params = params ))
68103
69104 folders = []
70- for item in resp . json () ['tags' ]:
105+ for item in response ['tags' ]:
71106 if item .get ('type' ) != 'folder' :
72107 continue
73108
@@ -78,17 +113,14 @@ def get_folders(self):
78113 return folders
79114
80115 def get_tags (self ):
81- if not self .auth_token :
82- raise NotLoginError
116+ self .check_token ()
83117
84- url = urljoin (BASE_URL , 'tag/list' )
118+ url = urljoin (BASE_URL , self . TAG_LIST_PATH )
85119 params = {'types' : 1 , 'counts' : 1 }
86- resp = self .session .post (url , params = params )
87- if resp .status_code != 200 :
88- raise APIError (resp .text )
120+ response = self .parse_response (self .session .post (url , params = params ))
89121
90122 tags = []
91- for item in resp . json () ['tags' ]:
123+ for item in response ['tags' ]:
92124 if item .get ('type' ) != 'tag' :
93125 continue
94126
@@ -99,15 +131,11 @@ def get_tags(self):
99131 return tags
100132
101133 def get_subscription_list (self ):
102- if not self .auth_token :
103- raise NotLoginError
104-
105- url = urljoin (BASE_URL , 'subscription/list' )
106- resp = self .session .get (url )
107- if resp .status_code != 200 :
108- raise APIError (resp .text )
134+ self .check_token ()
109135
110- for item in resp .json ()['subscriptions' ]:
136+ url = urljoin (BASE_URL , self .SUBSCRIPTION_LIST_PATH )
137+ response = self .parse_response (self .session .get (url ))
138+ for item in response ['subscriptions' ]:
111139 yield Subscription .from_json (item )
112140
113141 def get_stream_contents (self , stream_id , c = '' ):
@@ -119,45 +147,34 @@ def get_stream_contents(self, stream_id, c=''):
119147 break
120148
121149 def __get_stream_contents (self , stream_id , continuation = '' ):
122- if not self .auth_token :
123- raise NotLoginError
150+ self .check_token ()
124151
125- url = urljoin (BASE_URL , 'stream/contents/' + quote_plus (stream_id ))
152+ url = urljoin (BASE_URL , self . STREAM_CONTENTS_PATH + quote_plus (stream_id ))
126153 params = {
127154 'n' : 50 , # default 20, max 1000
128155 'r' : '' ,
129156 'c' : continuation ,
130157 'output' : 'json'
131158 }
132- resp = self .session .post (url , params = params )
133- if resp .status_code != 200 :
134- raise APIError (resp .text )
135-
136- if 'continuation' in resp .json ():
137- return resp .json ()['items' ], resp .json ()['continuation' ]
159+ response = self .parse_response (self .session .post (url , params = params ))
160+ if 'continuation' in response ():
161+ return response ['items' ], response ['continuation' ]
138162 else :
139- return resp . json () ['items' ], None
163+ return response ['items' ], None
140164
141165 def fetch_unread (self , folder = None , tags = None ):
142- if not self .auth_token :
143- raise NotLoginError
166+ self .check_token ()
144167
145- url = urljoin (BASE_URL , 'stream/contents/' )
168+ url = urljoin (BASE_URL , self . STREAM_CONTENTS_PATH )
146169 if folder :
147170 url = urljoin (
148171 url ,
149- quote_plus ('user/{}/label/{}' . format (self . userid , folder ))
172+ quote_plus (self . GENERAL_TAG_TEMPLATE . format (folder ))
150173 )
151- params = {
152- 'xt' : 'user/{}/state/com.google/read' .format (self .userid ),
153- 'c' : str (uuid4 ())
154- }
174+ params = {'xt' : self .READ_TAG , 'c' : str (uuid4 ())}
155175
156- resp = self .session .post (url , params = params )
157- if resp .status_code != 200 :
158- raise APIError (resp .text )
159-
160- for data in resp .json ()['items' ]:
176+ response = self .parse_response (self .session .post (url , params = params ))
177+ for data in response ['items' ]:
161178 categories = set ([
162179 category .split ('/' )[- 1 ] for category in data .get ('categories' , [])
163180 if category .find ('label' ) > 0
@@ -166,48 +183,43 @@ def fetch_unread(self, folder=None, tags=None):
166183 continue
167184 yield Article .from_json (data )
168185
169- continuation = resp . json () .get ('continuation' )
186+ continuation = response .get ('continuation' )
170187 while continuation :
171188 params ['c' ] = continuation
172- resp = self .session .post (url , params = params )
173- if resp .status_code != 200 :
174- raise APIError (resp .text )
175- for data in resp .json ()['items' ]:
189+ response = self .parse_response (self .session .post (url , params = params ))
190+ for data in response ['items' ]:
176191 categories = set ([
177192 category .split ('/' )[- 1 ] for category in data .get ('categories' , [])
178193 if category .find ('label' ) > 0
179194 ])
180195 if tags and not categories .issuperset (set (tags )):
181196 continue
182197 yield Article .from_json (data )
183- continuation = resp . json () .get ('continuation' )
198+ continuation = response .get ('continuation' )
184199
185200 def add_general_label (self , articles , label ):
186- if not self .auth_token :
187- raise NotLoginError
201+ self .check_token ()
188202
189- url = urljoin (BASE_URL , 'edit-tag' )
203+ url = urljoin (BASE_URL , self . EDIT_TAG_PATH )
190204 for start in range (0 , len (articles ), 10 ):
191205 end = min (start + 10 , len (articles ))
192206 params = {
193207 'a' : label ,
194208 'i' : [articles [idx ].id for idx in range (start , end )]
195209 }
196- resp = self .session .post (url , params = params )
197- if resp .status_code != 200 :
198- raise APIError (resp .text )
210+ self .parse_response (self .session .post (url , params = params ), json_data = False )
199211
200212 def add_tag (self , articles , tag ):
201- self .add_general_label (articles , 'user/-/label/{}' .format (tag ))
213+ self .add_general_label (articles , self . GENERAL_TAG_TEMPLATE .format (tag ))
202214
203215 def mark_as_read (self , articles ):
204- self .add_general_label (articles , 'user/-/state/com.google/read' )
216+ self .add_general_label (articles , self . READ_TAG )
205217
206218 def mark_as_starred (self , articles ):
207- self .add_general_label (articles , 'user/-/state/com.google/starred' )
219+ self .add_general_label (articles , self . STARRED_TAG )
208220
209221 def mark_as_liked (self , articles ):
210- self .add_general_label (articles , 'user/-/state/com.google/like' )
222+ self .add_general_label (articles , self . LIKED_TAG )
211223
212224 def broadcast (self , articles ):
213- self .add_general_label (articles , 'user/-/state/com.google/broadcast' )
225+ self .add_general_label (articles , self . BROADCAST_TAG )
0 commit comments