source: trunk/StreamVision/StreamVision.py@ 646

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

StreamVision.py: When we can't see the AirPlay button, assume AirPlay is in use.

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