merge
[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         print "location error: %d... quitting" % error
118         data.quit()
119
120     def on_changed(self, device, data):
121         if not device:
122             return
123         if device.fix:
124             if device.fix[1] & location.GPS_DEVICE_LATLONG_SET:
125                 self.lat, self.long = device.fix[4:6]
126                 data.stop()
127
128     def on_stop(self, control, data):
129         print "quitting"
130         data.quit()
131
132     def start_location(self, data):
133         data.start()
134         return False
135
136     def reset(self):
137         """ Reset coordinates """
138         self.device.reset_last_known()
139
140 class GigFinder:
141
142     def __init__(self):
143         self.lat = None
144         self.long = None
145         self.url_base = "http://ws.audioscrobbler.com/2.0/"
146         self.api_key = "1928a14bdf51369505530949d8b7e1ee"
147         self.distance = '10'
148         self.banner = None
149         self.parser = GigParser()
150         self.location = LocationUpdater()
151
152         program = hildon.Program.get_instance()
153         self.win = hildon.StackableWindow()
154         self.win.set_title('Gig Finder')
155         self.win.connect("destroy", gtk.main_quit, None)
156
157         pannable_area = hildon.PannableArea()
158
159         self.table = gtk.Table(columns=1)
160         self.table.set_row_spacings(10)
161         self.table.set_col_spacings(10)
162
163         pannable_area.add_with_viewport(self.table)
164
165         self.win.add(pannable_area)
166
167         Thread(target=self.update_gigs).start()
168
169     def main(self):
170         menu = self.create_menu()
171         self.win.set_app_menu(menu)
172         self.win.show_all()
173         gtk.main()
174
175     def update_gigs(self):
176         """ Get gig info """
177         gobject.idle_add(self.show_message, "Getting events")
178         gobject.idle_add(self.location.update_location)
179
180         # if no gps fix wait
181         while not self.location.lat:
182             time.sleep(1)
183         
184         events = self.get_events(self.location.lat, self.location.long)
185         gobject.idle_add(self.hide_message)
186         gobject.idle_add(self.show_events, events)
187
188     def show_message(self, message):
189         """ set window progress indicator and show message """
190         hildon.hildon_gtk_window_set_progress_indicator(self.win, 1)
191         self.banner = hildon.hildon_banner_show_information(self.win,
192                                                             '', 
193                                                             message)
194
195     def hide_message(self):
196         """ hide banner and sete progress indicator """
197         self.banner.hide()
198         hildon.hildon_gtk_window_set_progress_indicator(self.win, 0)
199
200     def get_events(self, lat, long):
201         """ retrieve xml and parse into events list """
202         xml = self.get_xml(lat, long)
203         events = self.parser.parse_xml(xml, 
204                                        lat,
205                                        long)
206         return events
207
208     def show_events(self, events):
209         """ sort events, set new window title and add events to table """
210         events = self.sort_gigs(events)
211         self.win.set_title('Gig Finder (%s)' % len(events))
212         self.add_events(events)
213
214     def distance_cmp(self, x, y):
215         """ compare distances for list sort """
216         if x > y:
217             return 1
218         elif x == y:
219             return 0
220         else:
221             return -1
222
223     def sort_gigs(self, events):
224         """ sort gig by distance """
225         events.sort(cmp=self.distance_cmp, key=lambda x: x['distance'])
226         return events
227         
228     def get_xml(self, lat, long):
229         """ Return xml from lastfm """
230         method = "geo.getevents"
231         params = urllib.urlencode({'method': method,
232                                    'api_key': self.api_key,
233                                    'distance': self.distance,
234                                    'long': long,
235                                    'lat': lat})
236         response = urllib.urlopen(self.url_base, params)
237         return response.read()
238
239     def create_menu(self):
240         """ build application menu """
241         menu = hildon.AppMenu()
242         button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
243         button.set_label('Placeholder')
244         menu.append(button)
245         menu.show_all()
246         return menu
247
248     def show_details(self, widget, data):
249         """ Open new window showing gig details """
250         win = hildon.StackableWindow()
251         win.set_title(data['title'])
252
253         win.vbox = gtk.VBox()
254         win.add(win.vbox)
255
256         scroll = hildon.PannableArea()
257         win.vbox.pack_start(scroll, True, True, 0)
258
259         view = hildon.TextView()
260         view.set_editable(False)
261         view.unset_flags(gtk.CAN_FOCUS)
262         view.set_wrap_mode(gtk.WRAP_WORD)
263         buffer = view.get_buffer()
264         end = buffer.get_end_iter()
265         buffer.insert(end, '%s\n' % data['title'])
266         buffer.insert(end, 'Artists: %s\n' % data['artists'])
267         buffer.insert(end, 'Venue: %s\n' % data['venue'])
268         buffer.insert(end, '%s\n' % data['address'])
269         buffer.insert(end, 'When: %s\n' % data['date'].strftime('%H:%M'))
270         buffer.insert(end, '\n')
271         scroll.add_with_viewport(view)
272
273         win.show_all()
274         
275     def add_events(self, events):
276         """ Add a table of buttons """
277         pos = 0
278         for event in events:
279             button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, 
280                                    hildon.BUTTON_ARRANGEMENT_VERTICAL)
281             button.set_text(event['title'], "distance: %0.02f km" % event['distance'])
282             button.connect("clicked", self.show_details, event)
283             self.table.attach(button, 0, 1, pos, pos+1)
284             pos += 1
285         self.table.show_all()
286    
287 if __name__ == "__main__":
288     finder = GigFinder()
289     finder.main()