source: trunk/StreamVision/StreamVision.py@ 632

Last change on this file since 632 was 632, checked in by Nicholas Riley, 13 years ago

StreamVision.py: With iTunes Store visible, some buttons can't have their AXDescription retrieved. Work around this.

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