source: trunk/StreamVision/StreamVision.py@ 486

Last change on this file since 486 was 486, checked in by Nicholas Riley, 16 years ago

StreamVision.py: Support Radio Paradise album cover retrieval.

File size: 12.1 KB
Line 
1#!/usr/bin/pythonw
2# -*- coding: utf-8 -*-
3
4from appscript import app, k, its, CommandError
5from AppKit import NSApplication, NSApplicationDefined, NSBeep, NSSystemDefined, NSURL, NSWorkspace
6from Foundation import NSDistributedNotificationCenter, NSSearchPathForDirectoriesInDomains, NSCachesDirectory, NSUserDomainMask
7from PyObjCTools import AppHelper
8from Carbon.CarbonEvt import RegisterEventHotKey, GetApplicationEventTarget
9from Carbon.Events import cmdKey, shiftKey, controlKey
10import httplib2
11import os
12import struct
13import scrape
14import HotKey
15
16GROWL_APP_NAME = 'StreamVision'
17NOTIFICATION_TRACK_INFO = 'iTunes Track Info'
18NOTIFICATIONS_ALL = [NOTIFICATION_TRACK_INFO]
19
20kEventHotKeyPressedSubtype = 6
21kEventHotKeyReleasedSubtype = 9
22
23kHIDUsage_Csmr_ScanNextTrack = 0xB5
24kHIDUsage_Csmr_ScanPreviousTrack = 0xB6
25kHIDUsage_Csmr_PlayOrPause = 0xCD
26
27def growlRegister():
28 global growl
29 growl = app(id='com.Growl.GrowlHelperApp')
30
31 growl.register(
32 as_application=GROWL_APP_NAME,
33 all_notifications=NOTIFICATIONS_ALL,
34 default_notifications=NOTIFICATIONS_ALL,
35 icon_of_application='iTunes.app')
36 # if we leave off the .app, we can get Classic iTunes's icon
37
38def growlNotify(title, description, **kw):
39 try:
40 growl.notify(
41 with_name=NOTIFICATION_TRACK_INFO,
42 title=title,
43 description=description,
44 application_name=GROWL_APP_NAME,
45 **kw)
46 except CommandError:
47 growlRegister()
48 growlNotify(title, description, **kw)
49
50def radioParadiseURL():
51 # XXX better to use http://www2.radioparadise.com/playlist.xml ?
52 session = scrape.Session()
53 session.go('http://www2.radioparadise.com/nowplay_b.php')
54 return session.region.firsttag('a')['href']
55
56def cleanStreamTitle(title):
57 if title == k.missing_value:
58 return ''
59 title = title.split(' [')[0] # XXX move to description
60 try: # incorrectly encoded?
61 title = title.encode('iso-8859-1').decode('utf-8')
62 except (UnicodeDecodeError, UnicodeEncodeError):
63 pass
64 title = title.replace('`', u'’')
65 return title
66
67def cleanStreamTrackName(name):
68 name = name.split('. ')[0]
69 name = name.split(': ')[0]
70 name = name.split(' - ')
71 if len(name) > 1:
72 name = ' - '.join(name[:-1])
73 else:
74 name = name[0]
75 return name
76
77def iTunesApp(): return app(id='com.apple.iTunes')
78def XTensionApp(): return app(creator='SHEx')
79def AmuaApp(): return app('Amua.app')
80
81HAVE_XTENSION = False
82try:
83 XTensionApp()
84 HAVE_XTENSION = True
85except:
86 pass
87
88HAVE_AMUA = False
89try:
90 AmuaApp()
91 HAVE_AMUA = True
92except:
93 pass
94
95needsStereoPowerOn = HAVE_XTENSION
96
97def amuaPlaying():
98 if not HAVE_AMUA:
99 return False
100 return AmuaApp().is_playing()
101
102class OneFileCache(object):
103 __slots__ = ('key', 'cache')
104
105 def __init__(self, cache):
106 if not os.path.exists(cache):
107 os.makedirs(cache)
108 self.cache = os.path.join(cache, 'file')
109 self.key = None
110
111 def get(self, key):
112 if key == self.key:
113 return file(self.cache, 'r').read()
114
115 def set(self, key, value):
116 self.key = key
117 file(self.cache, 'w').write(value)
118
119 def delete(self, key):
120 if key == self.key:
121 self.key = None
122 os.remove(cache)
123
124class StreamVision(NSApplication):
125
126 hotKeyActions = {}
127 hotKeys = []
128
129 def displayTrackInfo(self, playerInfo=None):
130 global needsStereoPowerOn
131
132 iTunes = iTunesApp()
133
134 trackClass = iTunes.current_track.class_()
135 trackName = ''
136 if trackClass != k.property:
137 trackName = iTunes.current_track.name()
138
139 if iTunes.player_state() != k.playing:
140 growlNotify('iTunes is not playing.', trackName)
141 if HAVE_XTENSION:
142 if not needsStereoPowerOn and XTensionApp().status('Stereo'):
143 XTensionApp().turnoff('Stereo')
144 needsStereoPowerOn = True
145 return
146 if needsStereoPowerOn:
147 if not XTensionApp().status('Stereo'):
148 XTensionApp().turnon('Stereo')
149 needsStereoPowerOn = False
150 if trackClass == k.URL_track:
151 if amuaPlaying():
152 if playerInfo is None: # Amua displays it itself
153 AmuaApp().display_song_information()
154 return
155 url = iTunes.current_stream_URL()
156 kw = {}
157 if url != k.missing_value and url.endswith('.jpg'):
158 response, content = self.http.request(url)
159 if response['content-type'].startswith('image/'):
160 file(self.imagePath, 'w').write(content)
161 kw['image_from_location'] = self.imagePath
162 growlNotify(cleanStreamTitle(iTunes.current_stream_title()),
163 cleanStreamTrackName(trackName), **kw)
164 return
165 if trackClass == k.property:
166 growlNotify('iTunes is playing.', '')
167 return
168 kw = {}
169 # XXX iTunes doesn't let you get artwork for shared tracks
170 if trackClass != k.shared_track:
171 artwork = iTunes.current_track.artworks()
172 if artwork:
173 kw['pictImage'] = artwork[0].data()
174 growlNotify(trackName + ' ' +
175 u'★' * (iTunes.current_track.rating() / 20),
176 iTunes.current_track.album() + '\n' +
177 iTunes.current_track.artist(),
178 **kw)
179
180 def goToSite(self):
181 iTunes = iTunesApp()
182 if iTunes.player_state() == k.playing:
183 if amuaPlaying():
184 AmuaApp().display_album_details()
185 return
186 url = iTunes.current_stream_URL()
187 if url != k.missing_value:
188 if 'radioparadise.com' in url and 'review' not in url:
189 url = radioParadiseURL()
190 NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(url))
191 return
192 NSBeep()
193
194 def registerHotKey(self, func, keyCode, mods=0):
195 hotKeyRef = RegisterEventHotKey(keyCode, mods, (0, 0),
196 GetApplicationEventTarget(), 0)
197 self.hotKeys.append(hotKeyRef)
198 self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)] = func
199 return hotKeyRef
200
201 def unregisterHotKey(self, hotKeyRef):
202 self.hotKeys.remove(hotKeyRef)
203 del self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)]
204 hotKeyRef.UnregisterEventHotKey()
205
206 def incrementRatingBy(self, increment):
207 iTunes = iTunesApp()
208 if amuaPlaying():
209 if increment < 0:
210 AmuaApp().ban_song()
211 growlNotify('Banned song.', '', icon_of_application='Amua.app')
212 else:
213 AmuaApp().love_song()
214 growlNotify('Loved song.', '', icon_of_application='Amua.app')
215 return
216 rating = iTunes.current_track.rating()
217 rating += increment
218 if rating < 0:
219 rating = 0
220 NSBeep()
221 elif rating > 100:
222 rating = 100
223 NSBeep()
224 iTunes.current_track.rating.set(rating)
225
226 def playPause(self, useStereo=True):
227 global needsStereoPowerOn
228
229 iTunes = iTunesApp()
230 was_playing = (iTunes.player_state() == k.playing)
231 if not useStereo:
232 needsStereoPowerOn = False
233 if was_playing and amuaPlaying():
234 AmuaApp().stop()
235 else:
236 iTunes.playpause()
237 if not was_playing and iTunes.player_state() == k.stopped:
238 # most likely, we're focused on the iPod, so playing does nothing
239 iTunes.browser_windows[1].view.set(iTunes.user_playlists[its.name=='Stations'][1]())
240 iTunes.play()
241 if HAVE_XTENSION and useStereo:
242 if iTunes.player_state() == k.playing:
243 XTensionApp().turnon('Stereo')
244 needsStereoPowerOn = False
245 else:
246 XTensionApp().turnoff('Stereo')
247 needsStereoPowerOn = True
248
249 def playPauseFront(self):
250 systemEvents = app(id='com.apple.systemEvents')
251 frontName = systemEvents.processes[its.frontmost == True][1].name()
252 if frontName == 'RealPlayer':
253 realPlayer = app(id='com.RealNetworks.RealPlayer')
254 if len(realPlayer.players()) > 0:
255 if realPlayer.players[1].state() == k.playing:
256 realPlayer.pause()
257 else:
258 realPlayer.play()
259 return
260 elif frontName == 'VLC':
261 app(id='org.videolan.vlc').play() # equivalent to playpause
262 else:
263 self.playPause(useStereo=False)
264
265 def nextTrack(self):
266 if amuaPlaying():
267 AmuaApp().skip_song()
268 return
269 iTunesApp().next_track()
270
271 def registerZoomWindowHotKey(self):
272 self.zoomWindowHotKey = self.registerHotKey(self.zoomWindow, 42, cmdKey | controlKey) # cmd-ctrl-\
273
274 def unregisterZoomWindowHotKey(self):
275 self.unregisterHotKey(self.zoomWindowHotKey)
276 self.zoomWindowHotKey = None
277
278 def zoomWindow(self):
279 # XXX detect if "enable access for assistive devices" needs to be enabled
280 systemEvents = app(id='com.apple.systemEvents')
281 frontName = systemEvents.processes[its.frontmost == True][1].name()
282 if frontName == 'iTunes':
283 systemEvents.processes['iTunes'].menu_bars[1]. \
284 menu_bar_items['Window'].menus.menu_items['Zoom'].click()
285 return
286 elif frontName in ('X11', 'Emacs'): # preserve C-M-\
287 self.unregisterZoomWindowHotKey()
288 systemEvents.key_code(42, using=[k.command_down, k.control_down])
289 self.registerZoomWindowHotKey()
290 return
291 frontPID = systemEvents.processes[its.frontmost == True][1].unix_id()
292 try:
293 zoomed = app(pid=frontPID).windows[1].zoomed
294 zoomed.set(not zoomed())
295 except (CommandError, RuntimeError):
296 systemEvents.processes[frontName].windows \
297 [its.subrole == 'AXStandardWindow'].windows[1]. \
298 buttons[its.subrole == 'AXZoomButton'].buttons[1].click()
299
300 def finishLaunching(self):
301 super(StreamVision, self).finishLaunching()
302
303 caches = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
304 NSUserDomainMask, True)[0]
305 cache = os.path.join(caches, 'StreamVision')
306 self.http = httplib2.Http(OneFileCache(cache), 5)
307 self.imagePath = os.path.join(cache, 'image')
308
309 self.registerHotKey(self.displayTrackInfo, 100) # F8
310 self.registerHotKey(self.goToSite, 100, cmdKey) # cmd-F8
311 self.registerHotKey(self.playPause, 101) # F9
312 self.registerHotKey(lambda: iTunesApp().previous_track(), 109) # F10
313 self.registerHotKey(self.nextTrack, 103) # F11
314 self.registerHotKey(lambda: self.incrementRatingBy(-20), 109, shiftKey) # shift-F10
315 self.registerHotKey(lambda: self.incrementRatingBy(20), 103, shiftKey) # shift-F11
316 self.registerZoomWindowHotKey()
317 NSDistributedNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, self.displayTrackInfo, 'com.apple.iTunes.playerInfo', None)
318 try:
319 import HIDRemote
320 HIDRemote.connect()
321 except ImportError:
322 print "failed to import HIDRemote (XXX fix - on Intel)"
323 except OSError, e:
324 print "failed to connect to remote: ", e
325
326 def sendEvent_(self, theEvent):
327 eventType = theEvent.type()
328 if eventType == NSSystemDefined and \
329 theEvent.subtype() == kEventHotKeyPressedSubtype:
330 self.hotKeyActions[theEvent.data1()]()
331 elif eventType == NSApplicationDefined:
332 key = theEvent.data1()
333 if key == kHIDUsage_Csmr_ScanNextTrack:
334 self.nextTrack()
335 elif key == kHIDUsage_Csmr_ScanPreviousTrack:
336 iTunesApp().previous_track()
337 elif key == kHIDUsage_Csmr_PlayOrPause:
338 self.playPauseFront()
339 super(StreamVision, self).sendEvent_(theEvent)
340
341if __name__ == "__main__":
342 growlRegister()
343 AppHelper.runEventLoop()
344 try:
345 HIDRemote.disconnect()
346 except:
347 pass
Note: See TracBrowser for help on using the repository browser.