Add code for location in scratchbox, fix ui bug when less than 10 results
[gigfinder] / src / opt / gigfinder / gigfinder.py
1 #!/usr/bin/env python
2
3 """Simple program to display local gigs
4
5 Intended for use on the N900, uses the devices gps to find local gigs.
6 """
7
8 __authors__ = ["Jon Staley"]
9 __copyright__ = "Copyright 2010 Jon Staley"
10 __license__ = "MIT"
11 __version__ = "0.0.1"
12
13 from xml.dom.minidom import parseString
14 from datetime import datetime, date
15 import urllib
16 import time
17 import gtk
18 import hildon
19 import location
20 import time
21 import gobject
22 import os.path
23 from threading import Thread
24 import thread
25
26 from locator import LocationUpdater
27
28 gtk.gdk.threads_init()
29
30 class GigParser:
31
32     def parse_xml(self, xml, lat, long):
33         """ Parse xml into a dict """
34         events_list = []
35         today = date.today()
36         dom = parseString(xml)
37
38         events = dom.getElementsByTagName('event')
39         for event in events:
40             start_date = self.parse_date(event.getElementsByTagName('startDate')[0].childNodes[0].data)
41             if start_date.date() == today:
42                 title = event.getElementsByTagName('title')[0].childNodes[0].data
43                 
44                 artists_element = event.getElementsByTagName('artists')[0]
45                 artist_list = []
46                 for artist in artists_element.getElementsByTagName('artist'):
47                     artist_list.append(artist.childNodes[0].data)
48                 artists = ', '.join(artist_list)
49
50                 venue_details = event.getElementsByTagName('venue')[0]
51                 venue_name = venue_details.getElementsByTagName('name')[0].childNodes[0].data
52                 address = self.get_address(venue_details.getElementsByTagName('location')[0])
53                 geo_data = venue_details.getElementsByTagName('geo:point')[0]
54                 venue_lat = geo_data.getElementsByTagName('geo:lat')[0].childNodes[0].data
55                 venue_long = geo_data.getElementsByTagName('geo:long')[0].childNodes[0].data
56                 distance = location.distance_between(float(lat), 
57                                                      float(long), 
58                                                      float(venue_lat), 
59                                                      float(venue_long))
60                 
61                 events_list.append({'title': title,
62                                     'venue': venue_name,
63                                     'address': address,
64                                     'distance': distance,
65                                     'artists': artists,
66                                     'date': start_date})
67         return events_list
68     
69     def get_address(self, location):
70         """ Return the venues address details from the xml element """
71         street = ''
72         city = ''
73         country = ''
74         postalcode = ''
75         if location.getElementsByTagName('street')[0].childNodes:
76             street = location.getElementsByTagName('street')[0].childNodes[0].data
77         if location.getElementsByTagName('city')[0].childNodes:
78             city = location.getElementsByTagName('city')[0].childNodes[0].data
79         if location.getElementsByTagName('country')[0].childNodes:
80             country = location.getElementsByTagName('country')[0].childNodes[0].data
81         if location.getElementsByTagName('postalcode')[0].childNodes:
82             postalcode = location.getElementsByTagName('postalcode')[0].childNodes[0].data
83         return '\n'.join([street, city, country, postalcode])
84
85     def parse_date(self, date_string):
86         """ Parse date string into datetime object """
87         fmt =  "%a, %d %b %Y %H:%M:%S"
88         result = time.strptime(date_string, fmt)
89         return datetime(result.tm_year, 
90                         result.tm_mon, 
91                         result.tm_mday, 
92                         result.tm_hour, 
93                         result.tm_min, 
94                         result.tm_sec)
95
96
97 class Events:
98
99     def __init__(self):
100         self.api_key = "1928a14bdf51369505530949d8b7e1ee"
101         self.url_base = "http://ws.audioscrobbler.com/2.0/"
102         self.parser = GigParser()
103
104     def get_events(self, lat, long, distance):
105         """ Retrieve xml and parse into events list """
106         events = []
107         for page in ['1', '2', '3']:
108             xml = self.get_xml(lat, long, distance, page=page)
109             events.extend(self.parser.parse_xml(xml, 
110                                                 lat,
111                                                 long))
112         return self.sort_events(events)
113
114     def sort_events(self, events):
115         """ Sort gig by distance """
116         events.sort(cmp=self.distance_cmp, key=lambda x: x['distance'])
117         return events
118         
119     def get_xml(self, lat, long, distance, page="1"):
120         """ Return xml from lastfm """
121         method = "geo.getevents"
122         params = urllib.urlencode({'method': method,
123                                    'api_key': self.api_key,
124                                    'distance': distance,
125                                    'long': long,
126                                    'lat': lat,
127                                    'page': page})
128         response = urllib.urlopen(self.url_base, params)
129         return response.read()
130     
131     def distance_cmp(self, x, y):
132         """ Compare distances for list sort """
133         if x > y:
134             return 1
135         elif x == y:
136             return 0
137         else:
138             return -1
139
140
141 class GigFinder:
142
143     def __init__(self):
144         self.lat = None
145         self.long = None
146         self.distance = '10'
147         self.banner = None
148         self.location = LocationUpdater()
149         self.events = Events()
150         self.win = hildon.StackableWindow()
151         self.app_title = "Gig Finder"
152         # TODO: 
153         # Add user settings for distance, date
154         # refactor gui code, 
155         # maybe do km to mile conversions
156
157     def main(self):
158         """ Build the gui and start the update thread """
159         program = hildon.Program.get_instance()
160         menu = self.create_menu()
161
162         self.win.set_title(self.app_title)
163         self.win.connect("destroy", gtk.main_quit, None)
164         self.win.set_app_menu(menu)
165
166         Thread(target=self.update_gigs).start()
167
168         self.win.show_all()
169         gtk.main()
170
171     def show_about(self, widget, data):
172         """ Show about dialog """
173         dialog = gtk.AboutDialog()
174         dialog.set_name('Gig Finder')
175         dialog.set_version(__version__)
176         dialog.set_authors(__authors__)
177         dialog.set_comments('Display gigs close by.\nUsing the http://www.last.fm api.')
178         dialog.set_license('Distributed under the MIT license.\nhttp://www.opensource.org/licenses/mit-license.php')
179         dialog.set_copyright(__copyright__)
180         dialog.show_all()
181
182     def update(self, widget, data):
183         """ Start update process """
184         self.win.set_title(self.app_title)
185         self.location.reset()
186         self.win.remove(self.pannable_area)
187         Thread(target=self.update_gigs).start()
188
189     def update_gigs(self):
190         """ Get gig info """
191         gobject.idle_add(self.show_message, "Getting events")
192         
193         if not 'applications' in os.path.abspath(__file__):
194             gobject.idle_add(self.location.update_location)
195
196             # if no gps fix wait
197             # TODO: needs a timeout
198             while not self.location.lat or not self.location.long:
199                 time.sleep(1)
200         else:
201             self.location.lat = float(51.517369)
202             self.location.long = float(-0.082998)
203
204         events = self.events.get_events(self.location.lat, 
205                                         self.location.long, 
206                                         self.distance)
207         gobject.idle_add(self.hide_message)
208         gobject.idle_add(self.show_events, events)
209         thread.exit()
210
211     def show_message(self, message):
212         """ Set window progress indicator and show message """
213         hildon.hildon_gtk_window_set_progress_indicator(self.win, 1)
214         self.banner = hildon.hildon_banner_show_information(self.win,
215                                                             '', 
216                                                             message)
217
218     def hide_message(self):
219         """ Hide banner and sete progress indicator """
220         self.banner.hide()
221         hildon.hildon_gtk_window_set_progress_indicator(self.win, 0)
222
223     def show_events(self, events):
224         """ Sort events, set new window title and add events to table """
225         if events:
226             self.win.set_title('%s (%s)' % (self.app_title, len(events)))
227             self.add_events(events)
228         else:
229             label = gtk.Label('No events available')
230             vbox = gtk.VBox(False, 0)
231             vbox.pack_start(label, True, True, 0)
232             vbox.show_all()
233             self.win.add(vbox)
234
235     def create_menu(self):
236         """ Build application menu """
237         update_button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
238         update_button.set_label('Update')
239         update_button.connect('clicked',
240                               self.update,
241                               None)
242
243         about_button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
244         about_button.set_label('About')
245         about_button.connect('clicked',
246                              self.show_about,
247                              None)
248
249         menu = hildon.AppMenu()
250         menu.append(update_button)
251         menu.append(about_button)
252         menu.show_all()
253         return menu
254
255     def show_details(self, widget, data):
256         """ Open new window showing gig details """
257         win = hildon.StackableWindow()
258         win.set_title(data['title'])
259
260         win.vbox = gtk.VBox()
261         win.add(win.vbox)
262
263         scroll = hildon.PannableArea()
264         win.vbox.pack_start(scroll, True, True, 0)
265
266         view = hildon.TextView()
267         view.set_editable(False)
268         view.unset_flags(gtk.CAN_FOCUS)
269         view.set_wrap_mode(gtk.WRAP_WORD)
270         buffer = view.get_buffer()
271         end = buffer.get_end_iter()
272         buffer.insert(end, '%s\n' % data['title'])
273         buffer.insert(end, 'Artists: %s\n' % data['artists'])
274         buffer.insert(end, 'Venue: %s\n' % data['venue'])
275         buffer.insert(end, '%s\n' % data['address'])
276         buffer.insert(end, 'When: %s\n' % data['date'].strftime('%H:%M %d/%M/%Y'))
277         buffer.insert(end, '\n')
278         scroll.add_with_viewport(view)
279
280         win.show_all()
281
282     def add_table(self):
283         """ Add table for events """
284         self.table = gtk.Table(columns=1)
285         self.table.set_row_spacings(10)
286         self.table.set_col_spacings(10)
287
288         self.pannable_area = hildon.PannableArea()
289         self.pannable_area.add_with_viewport(self.table)
290         self.pannable_area.show_all()
291         self.win.add(self.pannable_area)
292         
293     def add_events(self, events):
294         """ Add a table of buttons """
295         self.add_table()
296         pos = 0
297         
298         for event in events:
299             button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, 
300                                    hildon.BUTTON_ARRANGEMENT_VERTICAL)
301             button.set_text(event['title'], "distance: %0.02f km" % event['distance'])
302             button.connect("clicked", self.show_details, event)
303             self.table.attach(button, 0, 1, pos, pos+1, yoptions=gtk.FILL)
304             pos += 1
305         self.table.show_all()
306             
307 if __name__ == "__main__":
308     finder = GigFinder()
309     finder.main()