source: trunk/StreamVision/StreamVision.py@ 644

Last change on this file since 644 was 644, checked in by Nicholas Riley, 12 years ago

StreamVision.py: Remove workaround for iTunes Store accessibility bug in older iTunes versions.

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