source: trunk/StreamVision/StreamVision.py@ 631

Last change on this file since 631 was 631, checked in by Nicholas Riley, 14 years ago

StreamVision.py: iTunes 10 takes a small step forward in accessibility. Block zoom in XQuartz too. Turn off stereo when switching back to local speakers.

File size: 12.6 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 mayUseStereo():
98 if not HAVE_XTENSION:
99 return False
100 systemEvents = app(id='com.apple.systemEvents')
101 remote_speakers = systemEvents.application_processes[u'iTunes'].windows[u'iTunes'].buttons[its.attributes['AXDescription'].value.endswith(u'remote speakers')].title()
102 return (remote_speakers and remote_speakers[0] != k.missing_value)
103
104def turnStereoOn():
105 global needsStereoPowerOn
106 if not mayUseStereo():
107 if HAVE_XTENSION and XTensionApp().status('Stereo'):
108 XTensionApp().turnoff('Stereo')
109 return
110 if not XTensionApp().status('Stereo'):
111 XTensionApp().turnon('Stereo')
112 needsStereoPowerOn = False
113
114def turnStereoOff():
115 global needsStereoPowerOn
116 if not mayUseStereo():
117 return
118 if not needsStereoPowerOn and XTensionApp().status('Stereo'):
119 XTensionApp().turnoff('Stereo')
120 needsStereoPowerOn = True
121
122def amuaPlaying():
123 if not HAVE_AMUA:
124 return False
125 return AmuaApp().is_playing()
126
127class OneFileCache(object):
128 __slots__ = ('key', 'cache')
129
130 def __init__(self, cache):
131 if not os.path.exists(cache):
132 os.makedirs(cache)
133 self.cache = os.path.join(cache, 'file')
134 self.key = None
135
136 def get(self, key):
137 if key == self.key:
138 return file(self.cache, 'r').read()
139
140 def set(self, key, value):
141 self.key = key
142 file(self.cache, 'w').write(value)
143
144 def delete(self, key):
145 if key == self.key:
146 self.key = None
147 os.remove(cache)
148
149class StreamVision(NSApplication):
150
151 hotKeyActions = {}
152 hotKeys = []
153
154 def displayTrackInfo(self, playerInfo=None):
155 iTunes = iTunesApp()
156
157 try:
158 trackClass = iTunes.current_track.class_()
159 except CommandError:
160 trackClass = k.property
161
162 trackName = ''
163 if trackClass != k.property:
164 trackName = iTunes.current_track.name()
165
166 if iTunes.player_state() != k.playing:
167 growlNotify('iTunes is not playing.', trackName)
168 turnStereoOff()
169 return
170 turnStereoOn()
171 if trackClass == k.URL_track:
172 if amuaPlaying():
173 if playerInfo is None: # Amua displays it itself
174 AmuaApp().display_song_information()
175 return
176 url = iTunes.current_stream_URL()
177 kw = {}
178 if url != k.missing_value and url.endswith('.jpg'):
179 response, content = self.http.request(url)
180 if response['content-type'].startswith('image/'):
181 file(self.imagePath, 'w').write(content)
182 kw['image_from_location'] = self.imagePath
183 growlNotify(cleanStreamTitle(iTunes.current_stream_title()),
184 cleanStreamTrackName(trackName), **kw)
185 return
186 if trackClass == k.property:
187 growlNotify('iTunes is playing.', '')
188 return
189 kw = {}
190 # XXX iTunes doesn't let you get artwork for shared tracks
191 if trackClass != k.shared_track:
192 artwork = iTunes.current_track.artworks()
193 if artwork:
194 try:
195 kw['pictImage'] = artwork[0].data()
196 except CommandError:
197 pass
198 growlNotify(trackName + ' ' +
199 u'★' * (iTunes.current_track.rating() / 20),
200 iTunes.current_track.album() + '\n' +
201 iTunes.current_track.artist(),
202 **kw)
203
204 def goToSite(self):
205 iTunes = iTunesApp()
206 if iTunes.player_state() == k.playing:
207 if amuaPlaying():
208 AmuaApp().display_album_details()
209 return
210 url = iTunes.current_stream_URL()
211 if url != k.missing_value:
212 if 'radioparadise.com' in url and 'review' not in url:
213 url = radioParadiseURL()
214 NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(url))
215 return
216 NSBeep()
217
218 def registerHotKey(self, func, keyCode, mods=0):
219 hotKeyRef = RegisterEventHotKey(keyCode, mods, (0, 0),
220 GetApplicationEventTarget(), 0)
221 self.hotKeys.append(hotKeyRef)
222 self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)] = func
223 return hotKeyRef
224
225 def unregisterHotKey(self, hotKeyRef):
226 self.hotKeys.remove(hotKeyRef)
227 del self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)]
228 hotKeyRef.UnregisterEventHotKey()
229
230 def incrementRatingBy(self, increment):
231 iTunes = iTunesApp()
232 if amuaPlaying():
233 if increment < 0:
234 AmuaApp().ban_song()
235 growlNotify('Banned song.', '', icon_of_application='Amua.app')
236 else:
237 AmuaApp().love_song()
238 growlNotify('Loved song.', '', icon_of_application='Amua.app')
239 return
240 rating = iTunes.current_track.rating()
241 rating += increment
242 if rating < 0:
243 rating = 0
244 NSBeep()
245 elif rating > 100:
246 rating = 100
247 NSBeep()
248 iTunes.current_track.rating.set(rating)
249
250 def playPause(self, useStereo=True):
251 global needsStereoPowerOn
252
253 iTunes = iTunesApp()
254 was_playing = (iTunes.player_state() == k.playing)
255 if not useStereo:
256 needsStereoPowerOn = False
257 if was_playing and amuaPlaying():
258 AmuaApp().stop()
259 else:
260 iTunes.playpause()
261 if not was_playing and iTunes.player_state() == k.stopped:
262 # most likely, we're focused on the iPod, so playing does nothing
263 iTunes.browser_windows[1].view.set(iTunes.user_playlists[its.name=='Stations'][1]())
264 iTunes.play()
265 if not useStereo:
266 return
267 if iTunes.player_state() == k.playing:
268 turnStereoOn()
269 else:
270 turnStereoOff()
271
272 def playPauseFront(self):
273 systemEvents = app(id='com.apple.systemEvents')
274 frontName = systemEvents.processes[its.frontmost == True][1].name()
275 if frontName == 'RealPlayer':
276 realPlayer = app(id='com.RealNetworks.RealPlayer')
277 if len(realPlayer.players()) > 0:
278 if realPlayer.players[1].state() == k.playing:
279 realPlayer.pause()
280 else:
281 realPlayer.play()
282 return
283 elif frontName == 'VLC':
284 app(id='org.videolan.vlc').play() # equivalent to playpause
285 else:
286 self.playPause(useStereo=False)
287
288 def nextTrack(self):
289 if amuaPlaying():
290 AmuaApp().skip_song()
291 return
292 iTunesApp().next_track()
293
294 def registerZoomWindowHotKey(self):
295 self.zoomWindowHotKey = self.registerHotKey(self.zoomWindow, 42, cmdKey | controlKey) # cmd-ctrl-\
296
297 def unregisterZoomWindowHotKey(self):
298 self.unregisterHotKey(self.zoomWindowHotKey)
299 self.zoomWindowHotKey = None
300
301 def zoomWindow(self):
302 # XXX detect if "enable access for assistive devices" needs to be enabled
303 systemEvents = app(id='com.apple.systemEvents')
304 frontName = systemEvents.processes[its.frontmost == True][1].name()
305 if frontName == 'iTunes':
306 systemEvents.processes['iTunes'].menu_bars[1]. \
307 menu_bar_items['Window'].menus.menu_items['Zoom'].click()
308 return
309 elif frontName in ('X11', 'XQuartz', 'Emacs'): # preserve C-M-\
310 self.unregisterZoomWindowHotKey()
311 systemEvents.key_code(42, using=[k.command_down, k.control_down])
312 self.registerZoomWindowHotKey()
313 return
314 frontPID = systemEvents.processes[its.frontmost == True][1].unix_id()
315 try:
316 zoomed = app(pid=frontPID).windows[1].zoomed
317 zoomed.set(not zoomed())
318 except (CommandError, RuntimeError):
319 systemEvents.processes[frontName].windows \
320 [its.subrole == 'AXStandardWindow'].windows[1]. \
321 buttons[its.subrole == 'AXZoomButton'].buttons[1].click()
322
323 def finishLaunching(self):
324 super(StreamVision, self).finishLaunching()
325
326 caches = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
327 NSUserDomainMask, True)[0]
328 cache = os.path.join(caches, 'StreamVision')
329 self.http = httplib2.Http(OneFileCache(cache), 5)
330 self.imagePath = os.path.join(cache, 'image')
331
332 self.registerHotKey(self.displayTrackInfo, 100) # F8
333 self.registerHotKey(self.goToSite, 100, cmdKey) # cmd-F8
334 self.registerHotKey(self.playPause, 101) # F9
335 self.registerHotKey(lambda: iTunesApp().previous_track(), 109) # F10
336 self.registerHotKey(self.nextTrack, 103) # F11
337 self.registerHotKey(lambda: self.incrementRatingBy(-20), 109, shiftKey) # shift-F10
338 self.registerHotKey(lambda: self.incrementRatingBy(20), 103, shiftKey) # shift-F11
339 self.registerZoomWindowHotKey()
340 NSDistributedNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, self.displayTrackInfo, 'com.apple.iTunes.playerInfo', None)
341 try:
342 import HIDRemote
343 HIDRemote.connect()
344 except ImportError:
345 print "failed to import HIDRemote (XXX fix - on Intel)"
346 except OSError, e:
347 print "failed to connect to remote: ", e
348
349 def sendEvent_(self, theEvent):
350 eventType = theEvent.type()
351 if eventType == NSSystemDefined and \
352 theEvent.subtype() == kEventHotKeyPressedSubtype:
353 self.hotKeyActions[theEvent.data1()]()
354 elif eventType == NSApplicationDefined:
355 key = theEvent.data1()
356 if key == kHIDUsage_Csmr_ScanNextTrack:
357 self.nextTrack()
358 elif key == kHIDUsage_Csmr_ScanPreviousTrack:
359 iTunesApp().previous_track()
360 elif key == kHIDUsage_Csmr_PlayOrPause:
361 self.playPauseFront()
362 super(StreamVision, self).sendEvent_(theEvent)
363
364if __name__ == "__main__":
365 growlRegister()
366 AppHelper.runEventLoop()
367 try:
368 HIDRemote.disconnect()
369 except:
370 pass
Note: See TracBrowser for help on using the repository browser.