Trophae v6
[trophae.git] / trophies.py
blob12b76caddb25b5e98e4a929d389c2da00b955ba2
1 #!/usr/bin/python
3 # PSN Trophy Card for Maemo 5
5 # The code is ugly because I hacked it together quick.
6 # -- thp, circa 2010-08-26
9 # Redistribution and use in source and binary forms, with or without
10 # modification, are permitted provided that the following conditions
11 # are met:
13 # 1. Redistributions of source code must retain the above copyright
14 # notice, this list of conditions and the following disclaimer.
16 # 2. Redistributions in binary form must reproduce the above copyright
17 # notice, this list of conditions and the following disclaimer in the
18 # documentation and/or other materials provided with the distribution.
20 # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
21 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
24 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
26 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
27 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
29 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30 # SUCH DAMAGE.
34 # Based on:
35 # PSN Public Trophy List Parser
36 # 2010-08-24 Thomas Perl <thp@thpinfo.com>
37 # see also: http://talk.maemo.org/showthread.php?t=59732
39 import socket
40 import sys
41 import re
42 from BeautifulSoup import BeautifulSoup
44 import urllib2
45 import gtk
46 import pango
47 import gobject
49 import os
50 import hashlib
52 try:
53 import hildon
54 ScrolledWindow = hildon.PannableArea
55 Window = hildon.StackableWindow
56 def Button(text):
57 b = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | \
58 gtk.HILDON_SIZE_FINGER_HEIGHT, \
59 hildon.BUTTON_ARRANGEMENT_VERTICAL)
60 b.set_title(text)
61 return b
62 Entry = lambda: hildon.Entry(0)
63 except:
64 ScrolledWindow = gtk.ScrolledWindow
65 Window = gtk.Window
66 Button = gtk.Button
67 Entry = gtk.Entry
69 LEVEL_ICON = 'http://us.playstation.com/playstation/img/profile_level_icon.png'
71 TROPHY_ICONS = (
72 'http://us.playstation.com/playstation/img/trophy-01-bronze.png',
73 'http://us.playstation.com/playstation/img/trophy-02-silver.png',
74 'http://us.playstation.com/playstation/img/trophy-03-gold.png',
75 'http://us.playstation.com/playstation/img/trophy-04-platinum.png',
78 def create_connection(address):
79 """Stolen from Python 2.6"""
80 msg = "getaddrinfo returns an empty list"
81 host, port = address
82 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
83 af, socktype, proto, canonname, sa = res
84 sock = None
85 try:
86 sock = socket.socket(af, socktype, proto)
87 sock.connect(sa)
88 return sock
90 except error, msg:
91 if sock is not None:
92 sock.close()
94 raise error, msg
96 def Progress(window, progress):
97 try:
98 hildon.hildon_gtk_window_set_progress_indicator(window, progress)
99 except:
100 pass
102 def hash(d):
103 h = hashlib.sha1()
104 h.update(d)
105 return h.hexdigest()
107 def GetContents(url):
108 """downloader with caching"""
109 if url.startswith('/playstation/PSNImageServlet?avtar='):
110 url = url[len('/playstation/PSNImageServlet?avtar='):]
111 basename = hash(url)
112 filename = os.path.join(os.path.expanduser('~/.cache/ps3trophy/'), basename)
114 if not os.path.exists(os.path.dirname(filename)):
115 os.makedirs(os.path.dirname(filename))
117 if os.path.exists(filename):
118 return open(filename, 'rb').read()
119 else:
120 data = urllib2.urlopen(url).read()
121 fp = open(filename, 'wb')
122 fp.write(data)
123 fp.close()
124 return data
126 def Header(text):
127 return gtk.Label(text)
129 def Label(text):
130 return gtk.Label(text)
132 def BigLabel(text, cb=None):
133 l = gtk.Label()
134 l.set_markup('<b><big>%s</big></b>' % (text,))
135 if cb is not None:
136 cb(l)
137 return l
139 def ProgressIndicator(progress):
140 pb = gtk.ProgressBar()
141 pb.set_text('%d%%' % (progress,))
142 pb.set_fraction(float(progress)/100.)
143 return pb
145 class Game(object):
146 def __init__(self, image, name, progress, trophies, \
147 *trophycount):
148 self.image = image
149 self.name = name
150 self.progress = progress
151 self.trophies = trophies
152 self.trophycount = trophycount
154 def __unicode__(self):
155 name = self.name.replace('\n', ' ').replace('\r', ' ')
156 if len(name) > 40:
157 name = name[:37] + '...'
158 return u'%-40s %3d%% - %2d trophies (%r)' % (name, self.progress, self.trophies, self.trophycount)
160 class PsnTrophyParser(object):
161 def __init__(self, username):
162 self.username = username
163 self.trophy_data = self.get_trophy_data()
164 self.profile_data = self.get_profile_data()
165 self.games = list(self.get_games())
166 self.parse_profile()
168 def get_session_id(self):
169 c = create_connection(('us.playstation.com', 80))
170 c.send("""HEAD /playstation/psn/profiles/%s HTTP/1.1\r
171 TE: deflate,gzip;q=0.3\r
172 Connection: TE, close\r
173 Host: us.playstation.com\r
174 User-Agent: lwp-request/5.834 libwww-perl/5.834\r
176 """%self.username)
177 response = c.recv(1024)
178 c.close()
179 for line in response.splitlines():
180 m = re.match('Set-Cookie: ([^=]*)=([^;]*)', line)
181 if m is not None:
182 name, value = m.groups()
183 if name == 'JSESSIONID':
184 return value
186 def get_profile_data(self):
187 c = create_connection(('us.playstation.com', 80))
188 c.send("""GET /playstation/psn/profiles/%s HTTP/1.0\r
189 Referer: http://us.playstation.com/publictrophy/index.htm?onlinename=%s\r
190 User-Agent: Wget/1.12 (linux-gnu)\r
191 Accept: */*\r
192 Host: us.playstation.com\r
193 Connection: Keep-Alive\r
194 Cookie: JSESSIONID=%s\r
196 """ % (self.username, self.username, self.get_session_id()))
197 response = []
198 d = c.recv(1024)
199 while d:
200 response.append(d)
201 d = c.recv(1024)
202 c.close()
203 return ''.join(response)
206 def get_trophy_data(self):
207 c = create_connection(('us.playstation.com', 80))
208 c.send("""GET /playstation/psn/profile/%s/get_ordered_trophies_data HTTP/1.0\r
209 Referer: http://us.playstation.com/playstation/psn/profiles/%s\r
210 User-Agent: Wget/1.12 (linux-gnu)\r
211 Accept: */*\r
212 Host: us.playstation.com\r
213 Connection: Keep-Alive\r
214 Cookie: JSESSIONID=%s\r
216 """ % (self.username, self.username, self.get_session_id()))
217 response = []
218 d = c.recv(1024)
219 while d:
220 response.append(d)
221 d = c.recv(1024)
222 c.close()
223 return ''.join(response)
225 def parse_profile(self):
226 s = BeautifulSoup(self.profile_data)
227 self.level = int(s.find('div', {'id':'leveltext'}).string.strip())
228 self.progress = int(s.find('div', {'class':'progresstext'}).string.strip()[:-1]) # strip the trailing "%" sign
229 self.totaltrophies = int(s.find('div', {'id': 'totaltrophies'}).find('div', {'id': 'text'}).string.strip())
230 self.avatar = s.find('div', {'id': 'id-avatar'}).find('img')['src']
232 def get_games(self):
233 s = BeautifulSoup(self.trophy_data)
235 for div in s.findAll('div', {'class': 'slot'}):
236 image = dict(div.find('img').attrs)['src']
237 name = div.find('span', {'class': 'gameTitleSortField'}).string
238 progress = int(div.find('span', {'class': \
239 'gameProgressSortField'}).string.strip())
240 trophies = int(div.find('span', {'class': \
241 'gameTrophyCountSortField'}).string.strip())
242 values = [int(d.string.strip()) for d in div.find('div', {'class': 'trophyholder trophycountholder'}).findAll('div', {'class': 'trophycontent'})]
243 yield Game(image, name, progress, trophies, *values)
245 class TrophyCard(gtk.Table):
246 def __init__(self, parser, parent):
247 gtk.Table.__init__(self, 4+len(parser.games), 8)
248 self._run_later = []
249 self._parent = parent
250 self.parser = parser
251 self.attach(self.Image(self.parser.avatar), 0, 2, 0, 1)
252 self.attach(BigLabel(self.parser.username), 0, 2, 1, 2)
253 self.attach(self.LevelIndicator(self.parser.level), 2, 4, 0, 1)
254 self.attach(ProgressIndicator(self.parser.progress), 2, 7, 1, 2)
255 self.attach(BigLabel('%d Trophies' % (parser.totaltrophies,)), 4, 7, 0, 1)
256 self.attach(gtk.HSeparator(), 0, 7, 2, 3)
257 self.attach(Header('Title'), 0, 1, 3, 4)
258 self.attach(Header('Progress'), 1, 2, 3, 4)
259 self.attach(Header('Trophies'), 2, 3, 3, 4)
260 for i in range(4):
261 self.attach(self.Image(TROPHY_ICONS[i]), 3+i, 4+i, 3, 4)
263 for i, game in enumerate(self.parser.games):
264 self.attach(self.Title(game.name, game.image), 0, 1, 4+i, 5+i)
265 self.attach(ProgressIndicator(game.progress), 1, 2, 4+i, 5+i)
266 self.attach(Label(str(game.trophies)), 2, 3, 4+i, 5+i)
267 for x in range(4):
268 self.attach(Label(str(game.trophycount[x])), 3+x, 4+x, 4+i, 5+i)
270 gobject.idle_add(self._run_later_once)
272 def _run_later_once(self):
273 if self._run_later:
274 task = self._run_later.pop(0)
275 task()
276 while gtk.events_pending():
277 gtk.main_iteration(False)
278 if not self._run_later:
279 Progress(self._parent, False)
280 return False
281 else:
282 return True
284 def Image(self, url, cb=None):
285 i = gtk.Image()
286 def x():
287 pbl = gtk.gdk.PixbufLoader()
288 pbl.write(GetContents(url))
289 pbl.close()
290 i.set_from_pixbuf(pbl.get_pixbuf())
291 if cb is not None:
292 cb(i)
293 self._run_later.append(x)
294 return i
296 def ScaledImage(self, url, factor, cb=None):
297 def x(i):
298 pb = i.get_pixbuf()
299 width, height = pb.get_width(), pb.get_height()
300 i.set_from_pixbuf(pb.scale_simple(\
301 int(float(width)*factor), \
302 int(float(height)*factor), \
303 gtk.gdk.INTERP_NEAREST))
304 im = self.Image(url, x)
305 return im
307 def LevelIndicator(self, level):
308 h = gtk.HBox()
309 h.add(self.Image(LEVEL_ICON, lambda i: i.set_alignment(1., .5)))
310 h.add(BigLabel(str(level), lambda l: l.set_alignment(0., .5)))
311 return h
313 def Title(self, text, icon):
314 hb = gtk.VBox()
315 hb.add(self.ScaledImage(icon, .4))
316 lb = gtk.Label()
317 lb.set_markup('<small><b>%s</b></small>' % (text,))
318 lb.set_ellipsize(pango.ELLIPSIZE_END)
319 #hb.add(lb)
320 return hb
323 if __name__ == '__main__':
324 def show_trophies(username):
325 print 'show_trophies', username
326 w = Window()
327 w.set_title('Trophies: ' + username)
328 w.show_all()
329 Progress(w, True)
330 while gtk.events_pending():
331 gtk.main_iteration(False)
333 parser = PsnTrophyParser(username)
334 w.resize(800, 580)
335 sw = ScrolledWindow()
336 sw.add_with_viewport(TrophyCard(parser, w))
337 w.add(sw)
338 w.show_all()
340 w1 = Window()
341 w1.set_title('PS3 Trophies')
342 hb = gtk.HBox()
343 hb.pack_start(gtk.Label('PSN Nickname:'), False, False)
344 eUsername = Entry()
345 hb.pack_start(eUsername)
346 btn = Button('Show trophy card')
347 btn.connect('clicked', lambda b: show_trophies(eUsername.get_text()))
348 hb.pack_start(btn, False, False)
349 vb = gtk.VBox()
350 vb.pack_start(gtk.Label(''), True, True)
351 vb.pack_start(hb, False, False)
352 footer = gtk.Label()
353 footer.set_markup('<small>(c) 2010 Thomas Perl &lt;thpinfo.com&gt; / Icon by mr_xzibit</small>')
354 footer.set_alignment(1., 1.)
355 vb.pack_start(footer, True, True)
356 w1.add(vb)
357 w1.connect('destroy', gtk.main_quit)
358 w1.show_all()
360 gtk.main()