0
|
1 |
# A simple RSS reader application.
|
|
2 |
|
|
3 |
# Copyright (c) 2005 Nokia Corporation
|
|
4 |
#
|
|
5 |
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6 |
# you may not use this file except in compliance with the License.
|
|
7 |
# You may obtain a copy of the License at
|
|
8 |
#
|
|
9 |
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10 |
#
|
|
11 |
# Unless required by applicable law or agreed to in writing, software
|
|
12 |
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13 |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14 |
# See the License for the specific language governing permissions and
|
|
15 |
# limitations under the License.
|
|
16 |
|
|
17 |
import anydbm
|
|
18 |
|
|
19 |
import e32
|
|
20 |
import appuifw
|
|
21 |
from key_codes import *
|
|
22 |
|
|
23 |
class RSSFeed:
|
|
24 |
def __init__(self,url,title=None):
|
|
25 |
self.url=url
|
|
26 |
if title is None:
|
|
27 |
title=url
|
|
28 |
self.listeners=[]
|
|
29 |
self.feed={'title': title,
|
|
30 |
'entries': [],
|
|
31 |
'busy': False}
|
|
32 |
self.updating=False
|
|
33 |
def listen(self,callback):
|
|
34 |
self.listeners.append(callback)
|
|
35 |
def _notify_listeners(self,*args):
|
|
36 |
for x in self.listeners:
|
|
37 |
x(*args)
|
|
38 |
def start_update(self):
|
|
39 |
if self.feed['busy']:
|
|
40 |
appuifw.note(u'Update already in progress','info')
|
|
41 |
return
|
|
42 |
self.feed['busy']=True
|
|
43 |
import thread
|
|
44 |
thread.start_new_thread(self._update,())
|
|
45 |
def _update(self):
|
|
46 |
import dumbfeedparser as feedparser
|
|
47 |
newfeed=feedparser.parse(self.url)
|
|
48 |
self.feed.update(newfeed)
|
|
49 |
self.feed['busy']=False
|
|
50 |
self._notify_listeners()
|
|
51 |
def __getitem__(self,key):
|
|
52 |
return self.feed.__getitem__(key)
|
|
53 |
|
|
54 |
class CachingRSSFeed(RSSFeed):
|
|
55 |
def __init__(self,cache,url,title=None):
|
|
56 |
RSSFeed.__init__(self,url,title)
|
|
57 |
self.cache=cache
|
|
58 |
if cache.has_key(url):
|
|
59 |
self.feed=eval(cache[url])
|
|
60 |
self.dirty=False
|
|
61 |
RSSFeed.listen(self,self._invalidate_cache)
|
|
62 |
def _invalidate_cache(self):
|
|
63 |
self.dirty=True
|
|
64 |
# This method can't simply be a listener called by the RSSFeed,
|
|
65 |
# since that call is done in a different thread and currently the
|
|
66 |
# e32dbm module can only be used from the same thread it was
|
|
67 |
# opened in.
|
|
68 |
def save(self):
|
|
69 |
if self.dirty:
|
|
70 |
self.cache[self.url]=repr(self.feed)
|
|
71 |
|
|
72 |
|
|
73 |
def format_feed(feed):
|
|
74 |
if feed['busy']:
|
|
75 |
busyflag='(loading) '
|
|
76 |
else:
|
|
77 |
busyflag=''
|
|
78 |
return unicode('%d: %s%s'%(len(feed['entries']),
|
|
79 |
busyflag,
|
|
80 |
feed['title']))
|
|
81 |
|
|
82 |
def handle_screen(mode):
|
|
83 |
appuifw.app.screen = mode
|
|
84 |
|
|
85 |
|
|
86 |
class ReaderApp:
|
|
87 |
def __init__(self,feedlist):
|
|
88 |
self.lock=e32.Ao_lock()
|
|
89 |
self.exit_flag=False
|
|
90 |
self.old_exit_key_handler=appuifw.app.exit_key_handler
|
|
91 |
self.old_app_body=appuifw.app.body
|
|
92 |
appuifw.app.exit_key_handler=self.handle_exit
|
|
93 |
self.feeds=feedlist
|
|
94 |
self.articleviewer=appuifw.Text()
|
|
95 |
self.feedmenu=appuifw.Listbox([u''],
|
|
96 |
self.handle_feedmenu_select)
|
|
97 |
self.articlemenu=appuifw.Listbox([u''],
|
|
98 |
self.handle_articlemenu_select)
|
|
99 |
screenmodemenu=(u'Screen mode',
|
|
100 |
((u'full', lambda:handle_screen('full')),
|
|
101 |
(u'large', lambda:handle_screen('large')),
|
|
102 |
(u'normal', lambda:handle_screen('normal'))))
|
|
103 |
self.statemap={
|
|
104 |
'feedmenu':
|
|
105 |
{'menu':[(u'Update this feed', self.handle_update),
|
|
106 |
(u'Update all feeds', self.handle_update_all),
|
|
107 |
(u'Debug',self.handle_debug),
|
|
108 |
screenmodemenu,
|
|
109 |
(u'Exit',self.abort)],
|
|
110 |
'exithandler': self.abort},
|
|
111 |
'articlemenu':
|
|
112 |
{'menu':[(u'Update this feed', self.handle_update),
|
|
113 |
(u'Update all feeds', self.handle_update_all),
|
|
114 |
screenmodemenu,
|
|
115 |
(u'Exit',self.abort)],
|
|
116 |
'exithandler':lambda:self.goto_state('feedmenu')},
|
|
117 |
'articleview':
|
|
118 |
{'menu':[(u'Next article',self.handle_next),
|
|
119 |
(u'Previous article',self.handle_prev),
|
|
120 |
screenmodemenu,
|
|
121 |
(u'Exit',self.abort)],
|
|
122 |
'exithandler':lambda:self.goto_state('articlemenu')}}
|
|
123 |
self.articleviewer.bind(EKeyDownArrow,self.handle_downarrow)
|
|
124 |
self.articleviewer.bind(EKeyUpArrow,self.handle_uparrow)
|
|
125 |
self.articleviewer.bind(EKeyLeftArrow,self.handle_exit)
|
|
126 |
self.articlemenu.bind(EKeyRightArrow,
|
|
127 |
self.handle_articlemenu_select)
|
|
128 |
self.articlemenu.bind(EKeyLeftArrow,self.handle_exit)
|
|
129 |
self.feedmenu.bind(EKeyRightArrow,self.handle_feedmenu_select)
|
|
130 |
for k in self.feeds:
|
|
131 |
k.listen(self.notify)
|
|
132 |
self.goto_state('feedmenu')
|
|
133 |
def abort(self):
|
|
134 |
self.exit_flag=True
|
|
135 |
self.lock.signal()
|
|
136 |
def close(self):
|
|
137 |
appuifw.app.menu=[]
|
|
138 |
appuifw.app.exit_key_handler=self.old_exit_key_handler
|
|
139 |
appuifw.app.body=self.old_app_body
|
|
140 |
def run(self):
|
|
141 |
try:
|
|
142 |
while not self.exit_flag:
|
|
143 |
self.lock.wait()
|
|
144 |
self.refresh()
|
|
145 |
finally:
|
|
146 |
self.close()
|
|
147 |
def notify(self):
|
|
148 |
self.lock.signal()
|
|
149 |
def refresh(self):
|
|
150 |
self.goto_state(self.state)
|
|
151 |
def goto_state(self,newstate):
|
|
152 |
# Workaround for a Series 60 bug: Prevent the cursor from
|
|
153 |
# showing up if the articleviewer widget is not visible.
|
|
154 |
self.articleviewer.focus=False
|
|
155 |
if newstate=='feedmenu':
|
|
156 |
self.feedmenu.set_list(
|
|
157 |
[format_feed(x) for x in self.feeds])
|
|
158 |
appuifw.app.body=self.feedmenu
|
|
159 |
appuifw.app.title=u'RSS reader'
|
|
160 |
elif newstate=='articlemenu':
|
|
161 |
if len(self.current_feed['entries'])==0:
|
|
162 |
if appuifw.query(u'Download articles now?','query'):
|
|
163 |
self.handle_update()
|
|
164 |
self.goto_state('feedmenu')
|
|
165 |
return
|
|
166 |
self.articlemenu.set_list(
|
|
167 |
[self.format_article_title(x)
|
|
168 |
for x in self.current_feed['entries']])
|
|
169 |
appuifw.app.body=self.articlemenu
|
|
170 |
appuifw.app.title=format_feed(self.current_feed)
|
|
171 |
elif newstate=='articleview':
|
|
172 |
self.articleviewer.clear()
|
|
173 |
self.articleviewer.add(
|
|
174 |
self.format_title_in_article(self.current_article()))
|
|
175 |
self.articleviewer.add(
|
|
176 |
self.format_article(self.current_article()))
|
|
177 |
self.articleviewer.set_pos(0)
|
|
178 |
appuifw.app.body=self.articleviewer
|
|
179 |
appuifw.app.title=self.format_article_title(
|
|
180 |
self.current_article())
|
|
181 |
else:
|
|
182 |
raise RuntimeError("Invalid state %s"%state)
|
|
183 |
appuifw.app.menu=self.statemap[newstate]['menu']
|
|
184 |
self.state=newstate
|
|
185 |
def current_article(self):
|
|
186 |
return self.current_feed['entries'][self.current_article_index]
|
|
187 |
def handle_update(self):
|
|
188 |
if self.state=='feedmenu':
|
|
189 |
self.current_feed=self.feeds[self.feedmenu.current()]
|
|
190 |
self.current_feed.start_update()
|
|
191 |
self.refresh()
|
|
192 |
def handle_update_all(self):
|
|
193 |
for k in self.feeds:
|
|
194 |
if not k['busy']:
|
|
195 |
k.start_update()
|
|
196 |
self.refresh()
|
|
197 |
def handle_feedmenu_select(self):
|
|
198 |
self.current_feed=self.feeds[self.feedmenu.current()]
|
|
199 |
self.goto_state('articlemenu')
|
|
200 |
def handle_articlemenu_select(self):
|
|
201 |
self.current_article_index=self.articlemenu.current()
|
|
202 |
self.goto_state('articleview')
|
|
203 |
def handle_debug(self):
|
|
204 |
import btconsole
|
|
205 |
btconsole.run('Entering debug mode.',locals())
|
|
206 |
def handle_next(self):
|
|
207 |
if (self.current_article_index >=
|
|
208 |
len(self.current_feed['entries'])-1):
|
|
209 |
return
|
|
210 |
self.current_article_index += 1
|
|
211 |
self.refresh()
|
|
212 |
def handle_prev(self):
|
|
213 |
if self.current_article_index == 0:
|
|
214 |
return
|
|
215 |
self.current_article_index -= 1
|
|
216 |
self.refresh()
|
|
217 |
def handle_downarrow(self):
|
|
218 |
article_length=self.articleviewer.len()
|
|
219 |
cursor_pos=self.articleviewer.get_pos()
|
|
220 |
if cursor_pos==article_length:
|
|
221 |
self.handle_next()
|
|
222 |
else:
|
|
223 |
self.articleviewer.set_pos(min(cursor_pos+100,
|
|
224 |
article_length))
|
|
225 |
def handle_uparrow(self):
|
|
226 |
cursor_pos=self.articleviewer.get_pos()
|
|
227 |
if cursor_pos==0:
|
|
228 |
self.handle_prev()
|
|
229 |
self.articleviewer.set_pos(self.articleviewer.len())
|
|
230 |
else:
|
|
231 |
self.articleviewer.set_pos(max(cursor_pos-100,0))
|
|
232 |
def format_title_in_article(self, article):
|
|
233 |
self.articleviewer.highlight_color = (255,240,80)
|
|
234 |
self.articleviewer.style = (appuifw.STYLE_UNDERLINE|
|
|
235 |
appuifw.HIGHLIGHT_ROUNDED)
|
|
236 |
self.articleviewer.font = 'title'
|
|
237 |
self.articleviewer.color = (0,0,255)
|
|
238 |
return unicode("%(title)s\n"%article)
|
|
239 |
|
|
240 |
def format_article(self, article):
|
|
241 |
self.articleviewer.highlight_color = (0,0,0)
|
|
242 |
self.articleviewer.style = 0
|
|
243 |
self.articleviewer.font = 'normal'
|
|
244 |
self.articleviewer.color = (0,0,0)
|
|
245 |
return unicode("%(summary)s"%article)
|
|
246 |
|
|
247 |
def format_article_title(self, article):
|
|
248 |
return unicode("%(title)s"%article)
|
|
249 |
def handle_exit(self):
|
|
250 |
self.statemap[self.state]['exithandler']()
|
|
251 |
|
|
252 |
class DummyFeed:
|
|
253 |
def __init__(self,data): self.data=data
|
|
254 |
def listen(self,callback): pass
|
|
255 |
def start_update(self): pass
|
|
256 |
def __getitem__(self,key): return self.data.__getitem__(key)
|
|
257 |
def save(self): pass
|
|
258 |
dummyfeed=DummyFeed({'title': 'Dummy feed',
|
|
259 |
'entries': [{'title':'Dummy story',
|
|
260 |
'summary':'Blah blah blah.'},
|
|
261 |
{'title':'Another dummy story',
|
|
262 |
'summary':'Foo, bar and baz.'}],
|
|
263 |
'busy': False})
|
|
264 |
|
|
265 |
def main():
|
|
266 |
old_title=appuifw.app.title
|
|
267 |
appuifw.app.title=u'RSS reader'
|
|
268 |
cache=anydbm.open(u'c:\\rsscache','c')
|
|
269 |
feeds=[ CachingRSSFeed(url='http://slashdot.org/index.rss',
|
|
270 |
title='Slashdot',
|
|
271 |
cache=cache),
|
|
272 |
CachingRSSFeed(url='http://news.bbc.co.uk/rss/newsonline_world_edition/front_page/rss091.xml',
|
|
273 |
title='BBC',
|
|
274 |
cache=cache),
|
|
275 |
dummyfeed]
|
|
276 |
app = ReaderApp(feeds)
|
|
277 |
# Import heavyweight modules in the background to improve application
|
|
278 |
# startup time.
|
|
279 |
def import_modules():
|
|
280 |
import dumbfeedparser as feedparser
|
|
281 |
import btconsole
|
|
282 |
import thread
|
|
283 |
thread.start_new_thread(import_modules,())
|
|
284 |
try:
|
|
285 |
app.run()
|
|
286 |
finally:
|
|
287 |
for feed in feeds:
|
|
288 |
feed.save()
|
|
289 |
cache.close()
|
|
290 |
appuifw.app.title=old_title
|
|
291 |
|
|
292 |
if __name__=='__main__':
|
|
293 |
main()
|
|
294 |
|