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