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