source: trunk/StreamVision/StreamVision.py@ 637

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

StreamVision.py: iTunes 10.1.2 uses "AirPlay" rather than "remote speakers" in button description.

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