Update doc strings, add placeholder buttons and move gui building into main()
[gigfinder] / gig_finder.py
1 #!/usr/bin/env python2.5
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 __author__ = "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 from threading import Thread
23 import thread
24
25 gtk.gdk.threads_init()
26
27 class GigParser:
28
29     def parse_xml(self, xml, lat, long):
30         """ Parse xml into a dict """
31         # TODO: filter to todays events only
32         events_list = []
33         today = date.today()
34         dom = parseString(xml)
35
36         events = dom.getElementsByTagName('event')
37         for event in events:
38             title = event.getElementsByTagName('title')[0].childNodes[0].data
39             
40             artists_element = event.getElementsByTagName('artists')[0]
41             artist_list = []
42             for artist in artists_element.getElementsByTagName('artist'):
43                 artist_list.append(artist.childNodes[0].data)
44             artists = ', '.join(artist_list)
45
46             venue_details = event.getElementsByTagName('venue')[0]
47             venue_name = venue_details.getElementsByTagName('name')[0].childNodes[0].data
48             address = self.get_address(venue_details.getElementsByTagName('location')[0])
49             geo_data = venue_details.getElementsByTagName('geo:point')[0]
50             venue_lat = geo_data.getElementsByTagName('geo:lat')[0].childNodes[0].data
51             venue_long = geo_data.getElementsByTagName('geo:long')[0].childNodes[0].data
52             distance = location.distance_between(float(lat), 
53                                                  float(long), 
54                                                  float(venue_lat), 
55                                                  float(venue_long))
56             
57             start_date = self.parse_date(event.getElementsByTagName('startDate')[0].childNodes[0].data)
58             
59             events_list.append({'title': title,
60                                 'venue': venue_name,
61                                 'address': address,
62                                 'distance': distance,
63                                 'artists': artists,
64                                 'date': start_date})
65         return events_list
66     
67     def get_address(self, location):
68         """ Return the venues address details from the xml element """
69         street = ''
70         city = ''
71         country = ''
72         postalcode = ''
73         if location.getElementsByTagName('street')[0].childNodes:
74             street = location.getElementsByTagName('street')[0].childNodes[0].data
75         if location.getElementsByTagName('city')[0].childNodes:
76             city = location.getElementsByTagName('city')[0].childNodes[0].data
77         if location.getElementsByTagName('country')[0].childNodes:
78             country = location.getElementsByTagName('country')[0].childNodes[0].data
79         if location.getElementsByTagName('postalcode')[0].childNodes:
80             postalcode = location.getElementsByTagName('postalcode')[0].childNodes[0].data
81         return '\n'.join([street, city, country, postalcode])
82
83     def parse_date(self, date_string):
84         """ Parse date string into datetime object """
85         fmt =  "%a, %d %b %Y %H:%M:%S"
86         result = time.strptime(date_string, fmt)
87         return datetime(result.tm_year, 
88                         result.tm_mon, 
89                         result.tm_mday, 
90                         result.tm_hour, 
91                         result.tm_min, 
92                         result.tm_sec)
93
94 class LocationUpdater:
95
96     def __init__(self):
97         self.lat = None
98         self.long = None
99         self.loop = gobject.MainLoop()
100
101         self.control = location.GPSDControl.get_default()
102         self.control.set_properties(preferred_method=location.METHOD_USER_SELECTED,
103                                preferred_interval=location.INTERVAL_DEFAULT)
104         self.control.connect("error-verbose", self.on_error, self.loop)
105         self.control.connect("gpsd-stopped", self.on_stop, self.loop)
106
107         self.device = location.GPSDevice()
108         self.device.connect("changed", self.on_changed, self.control)
109
110     def update_location(self):
111         """ Run the loop and update lat and long """
112         self.reset()
113         gobject.idle_add(self.start_location, self.control)
114         self.loop.run()
115
116     def on_error(self, control, error, data):
117         """ Handle errors """
118         print "location error: %d... quitting" % error
119         data.quit()
120
121     def on_changed(self, device, data):
122         """ Set long and lat """
123         if not device:
124             return
125         if device.fix:
126             # once fix is found and long, lat available set long lat
127             if device.fix[1] & location.GPS_DEVICE_LATLONG_SET:
128                 self.lat, self.long = device.fix[4:6]
129                 data.stop()
130
131     def on_stop(self, control, data):
132         """ Stop the location service """
133         print "quitting"
134         data.quit()
135
136     def start_location(self, data):
137         """ Start the location service """
138         data.start()
139         return False
140
141     def reset(self):
142         """ Reset coordinates """
143         self.device.reset_last_known()
144
145 class GigFinder:
146
147     def __init__(self):
148         self.lat = None
149         self.long = None
150         self.url_base = "http://ws.audioscrobbler.com/2.0/"
151         self.api_key = "1928a14bdf51369505530949d8b7e1ee"
152         self.distance = '10'
153         self.banner = None
154         self.parser = GigParser()
155         self.location = LocationUpdater()
156         self.win = hildon.StackableWindow()
157
158     def main(self):
159         """ Build the gui and start the update thread """
160         program = hildon.Program.get_instance()
161         menu = self.create_menu()
162
163         self.table = gtk.Table(columns=1)
164         self.table.set_row_spacings(10)
165         self.table.set_col_spacings(10)
166
167         pannable_area = hildon.PannableArea()
168         pannable_area.add_with_viewport(self.table)
169
170         self.win.set_title('Gig Finder')
171         self.win.connect("destroy", gtk.main_quit, None)
172         self.win.set_app_menu(menu)
173         self.win.add(pannable_area)
174
175         Thread(target=self.update_gigs).start()
176
177         self.win.show_all()
178         gtk.main()
179
180     def update_gigs(self):
181         """ Get gig info """
182         gobject.idle_add(self.show_message, "Getting events")
183         gobject.idle_add(self.location.update_location)
184
185         # if no gps fix wait
186         while not self.location.lat:
187             time.sleep(1)
188         
189         events = self.get_events(self.location.lat, self.location.long)
190         gobject.idle_add(self.hide_message)
191         gobject.idle_add(self.show_events, events)
192
193     def show_message(self, message):
194         """ Set window progress indicator and show message """
195         hildon.hildon_gtk_window_set_progress_indicator(self.win, 1)
196         self.banner = hildon.hildon_banner_show_information(self.win,
197                                                             '', 
198                                                             message)
199
200     def hide_message(self):
201         """ Hide banner and sete progress indicator """
202         self.banner.hide()
203         hildon.hildon_gtk_window_set_progress_indicator(self.win, 0)
204
205     def get_events(self, lat, long):
206         """ Retrieve xml and parse into events list """
207         xml = self.get_xml(lat, long)
208         events = self.parser.parse_xml(xml, 
209                                        lat,
210                                        long)
211         return events
212
213     def show_events(self, events):
214         """ Sort events, set new window title and add events to table """
215         events = self.sort_gigs(events)
216         self.win.set_title('Gig Finder (%s)' % len(events))
217         self.add_events(events)
218
219     def distance_cmp(self, x, y):
220         """ Compare distances for list sort """
221         if x > y:
222             return 1
223         elif x == y:
224             return 0
225         else:
226             return -1
227
228     def sort_gigs(self, events):
229         """ Sort gig by distance """
230         events.sort(cmp=self.distance_cmp, key=lambda x: x['distance'])
231         return events
232         
233     def get_xml(self, lat, long):
234         """ Return xml from lastfm """
235         method = "geo.getevents"
236         params = urllib.urlencode({'method': method,
237                                    'api_key': self.api_key,
238                                    'distance': self.distance,
239                                    'long': long,
240                                    'lat': lat})
241         response = urllib.urlopen(self.url_base, params)
242         return response.read()
243
244     def create_menu(self):
245         """ Build application menu """
246         update_button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
247         update_button.set_label('Update')
248
249         about_button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
250         about_button.set_label('About')
251
252         menu = hildon.AppMenu()
253         menu.append(update_button)
254         menu.append(about_button)
255         menu.show_all()
256         return menu
257
258     def show_details(self, widget, data):
259         """ Open new window showing gig details """
260         win = hildon.StackableWindow()
261         win.set_title(data['title'])
262
263         win.vbox = gtk.VBox()
264         win.add(win.vbox)
265
266         scroll = hildon.PannableArea()
267         win.vbox.pack_start(scroll, True, True, 0)
268
269         view = hildon.TextView()
270         view.set_editable(False)
271         view.unset_flags(gtk.CAN_FOCUS)
272         view.set_wrap_mode(gtk.WRAP_WORD)
273         buffer = view.get_buffer()
274         end = buffer.get_end_iter()
275         buffer.insert(end, '%s\n' % data['title'])
276         buffer.insert(end, 'Artists: %s\n' % data['artists'])
277         buffer.insert(end, 'Venue: %s\n' % data['venue'])
278         buffer.insert(end, '%s\n' % data['address'])
279         buffer.insert(end, 'When: %s\n' % data['date'].strftime('%H:%M'))
280         buffer.insert(end, '\n')
281         scroll.add_with_viewport(view)
282
283         win.show_all()
284         
285     def add_events(self, events):
286         """ Add a table of buttons """
287         pos = 0
288         for event in events:
289             button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, 
290                                    hildon.BUTTON_ARRANGEMENT_VERTICAL)
291             button.set_text(event['title'], "distance: %0.02f km" % event['distance'])
292             button.connect("clicked", self.show_details, event)
293             self.table.attach(button, 0, 1, pos, pos+1)
294             pos += 1
295         self.table.show_all()
296    
297 if __name__ == "__main__":
298     finder = GigFinder()
299     finder.main()