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