source: trunk/StreamVision/StreamVision.py@ 666

Last change on this file since 666 was 666, checked in by Nicholas Riley, 11 years ago

StreamVision.py: Send all images to Growl as TIFF.

Previously we used whatever iTunes returned to us, which could be a
JPEG (no longer supported in current Growl versions), or a file path
(not supported with sandboxing).

http://growl.info/documentation/applescript-support.php
https://groups.google.com/forum/#!topic/growldiscuss/GbtPcpgSoNY

Thanks to Rudy Richter for the help.

File size: 14.4 KB
Line 
1#!/usr/bin/pythonw
2# -*- coding: utf-8 -*-
3
4from aem.ae import newdesc
5from appscript import app, k, its, CommandError
6from AppKit import (NSApplication, NSApplicationDefined, NSBeep, NSImage,
7 NSSystemDefined, NSURL, NSWorkspace)
8from Foundation import (NSDistributedNotificationCenter,
9 NSSearchPathForDirectoriesInDomains,
10 NSCachesDirectory, NSUserDomainMask)
11from PyObjCTools import AppHelper
12from Carbon.CarbonEvt import RegisterEventHotKey, GetApplicationEventTarget
13from Carbon.Events import cmdKey, shiftKey, controlKey
14from AudioDevice import (default_output_device_is_airplay,
15 set_default_output_device_changed_callback)
16import httplib2
17import os
18import struct
19import scrape
20import HotKey
21
22GROWL_APP_NAME = 'StreamVision'
23NOTIFICATION_TRACK_INFO = 'iTunes Track Info'
24NOTIFICATIONS_ALL = [NOTIFICATION_TRACK_INFO]
25
26kEventHotKeyPressedSubtype = 6
27kEventHotKeyReleasedSubtype = 9
28
29kHIDUsage_Csmr_ScanNextTrack = 0xB5
30kHIDUsage_Csmr_ScanPreviousTrack = 0xB6
31kHIDUsage_Csmr_PlayOrPause = 0xCD
32
33def growlRegister():
34 global growl
35 growl = app(id='com.Growl.GrowlHelperApp')
36
37 growl.register(
38 as_application=GROWL_APP_NAME,
39 all_notifications=NOTIFICATIONS_ALL,
40 default_notifications=NOTIFICATIONS_ALL,
41 icon_of_application='iTunes.app')
42 # if we leave off the .app, we can get Classic iTunes's icon
43
44def growlNotify(title, description, **kw):
45 try:
46 if usingStereo:
47 description += '\n(AirPlay)'
48
49 if 'image' in kw:
50 image = (NSImage.alloc().initWithData_(buffer(kw['image']))
51 .TIFFRepresentation())
52 kw['image'] = newdesc('TIFF', image)
53
54 growl.notify(
55 with_name=NOTIFICATION_TRACK_INFO,
56 title=title,
57 description=description,
58 application_name=GROWL_APP_NAME,
59 **kw)
60 except CommandError:
61 growlRegister()
62 growlNotify(title, description, **kw)
63
64def radioParadiseURL():
65 session = scrape.Session()
66 session.go('http://radioparadise.com/jq_playlist.php')
67 url = session.region.firsttag('a')['href']
68 if not url.startswith('http'):
69 url = 'http://www.radioparadise.com/rp2-' + url
70 return url
71
72def cleanStreamTitle(title):
73 if title == k.missing_value:
74 return ''
75 title = title.split(' [')[0] # XXX move to description
76 try: # incorrectly encoded?
77 title = title.encode('iso-8859-1').decode('utf-8')
78 except (UnicodeDecodeError, UnicodeEncodeError):
79 pass
80 title = title.replace('`', u'’')
81 return title
82
83def cleanStreamTrackName(name):
84 name = name.split('. ')[0]
85 name = name.split(': ')[0]
86 name = name.split(' - ')
87 if len(name) > 1:
88 name = ' - '.join(name[:-1])
89 else:
90 name = name[0]
91 return name
92
93def iTunesApp(): return app(id='com.apple.iTunes')
94def XTensionApp(): return app(creator='SHEx')
95def AmuaApp(): return app('Amua.app')
96
97HAVE_XTENSION = False
98try:
99 XTensionApp()
100 HAVE_XTENSION = True
101except:
102 pass
103
104HAVE_AMUA = False
105try:
106 AmuaApp()
107 HAVE_AMUA = True
108except:
109 pass
110
111needsStereoPowerOn = HAVE_XTENSION
112usingStereo = False
113
114def mayUseStereo():
115 if not HAVE_XTENSION:
116 return False
117 try:
118 # A bit better in iTunes 11.0.3, but can't do this via an Apple
119 # Event descriptor; have to send multiple events
120 return any(d.kind() != k.computer
121 for d in iTunesApp().current_AirPlay_devices())
122 except AttributeError:
123 pass
124
125 systemEvents = app(id='com.apple.systemEvents')
126 iTunesWindow = systemEvents.application_processes[u'iTunes'].windows[u'iTunes']
127 # Can't get AirPlay status with iTunes Mini Player or window on other Space.
128 try:
129 remote_speakers = iTunesWindow.buttons[its.attributes['AXDescription'].value.beginswith(u'AirPlay')].title()
130 except CommandError: # window on another Space?
131 return usingStereo
132 return remote_speakers and remote_speakers[0] not in (None, k.missing_value)
133
134def turnStereoOnOrOff():
135 global needsStereoPowerOn, usingStereo
136 usingStereo = False
137 if not default_output_device_is_airplay() and not mayUseStereo():
138 if HAVE_XTENSION and XTensionApp().status('Stereo'):
139 XTensionApp().turnoff('Stereo')
140 return
141 if not XTensionApp().status('Stereo'):
142 XTensionApp().turnon('Stereo')
143 usingStereo = True
144 needsStereoPowerOn = False
145
146def turnStereoOff():
147 global needsStereoPowerOn, usingStereo
148 usingStereo = False
149 if default_output_device_is_airplay() or not mayUseStereo():
150 return
151 if not needsStereoPowerOn and XTensionApp().status('Stereo'):
152 XTensionApp().turnoff('Stereo')
153 needsStereoPowerOn = True
154
155def amuaPlaying():
156 if not HAVE_AMUA:
157 return False
158 return AmuaApp().is_playing()
159
160class OneFileCache(object):
161 __slots__ = ('key', 'cache')
162
163 def __init__(self, cache):
164 if not os.path.exists(cache):
165 os.makedirs(cache)
166 self.cache = os.path.join(cache, 'file')
167 self.key = None
168
169 def get(self, key):
170 if key == self.key:
171 return file(self.cache, 'r').read()
172
173 def set(self, key, value):
174 self.key = key
175 file(self.cache, 'w').write(value)
176
177 def delete(self, key):
178 if key == self.key:
179 self.key = None
180 os.remove(cache)
181
182class StreamVision(NSApplication):
183
184 hotKeyActions = {}
185 hotKeys = []
186
187 def displayTrackInfo(self, playerInfo=None):
188 iTunes = iTunesApp()
189
190 try:
191 trackClass = iTunes.current_track.class_()
192 except CommandError:
193 trackClass = k.property
194
195 trackName = ''
196 if trackClass != k.property:
197 trackName = iTunes.current_track.name()
198
199 try:
200 playerState = iTunes.player_state()
201 except CommandError:
202 playerState = None # probably iTunes quit
203 if playerState != k.playing:
204 if playerState != None:
205 growlNotify('iTunes is not playing.', trackName)
206 turnStereoOff()
207 return
208 turnStereoOnOrOff()
209 if trackClass == k.URL_track:
210 if amuaPlaying():
211 if playerInfo is None: # Amua displays it itself
212 AmuaApp().display_song_information()
213 return
214 url = iTunes.current_stream_URL()
215 kw = {}
216 if url != k.missing_value and url.endswith('.jpg'):
217 try:
218 response, content = self.http.request(url)
219 except Exception, e:
220 import sys
221 print >> sys.stderr, 'Request for album art failed:', e
222 else:
223 if response['content-type'].startswith('image/'):
224 kw['image'] = content
225 growlNotify(cleanStreamTitle(iTunes.current_stream_title()),
226 cleanStreamTrackName(trackName), **kw)
227 return
228 if trackClass == k.property:
229 growlNotify('iTunes is playing.', '')
230 return
231 kw = {}
232 # XXX iTunes doesn't let you get artwork for shared tracks
233 if trackClass != k.shared_track:
234 artwork = iTunes.current_track.artworks()
235 if artwork:
236 try:
237 kw['image'] = artwork[0].data_().data
238 except CommandError:
239 pass
240 growlNotify(trackName + ' ' +
241 u'★' * (iTunes.current_track.rating() / 20),
242 iTunes.current_track.album() + '\n' +
243 iTunes.current_track.artist(),
244 **kw)
245
246 def defaultOutputDeviceChanged(self):
247 turnStereoOnOrOff()
248 self.displayTrackInfo()
249
250 def goToSite(self):
251 iTunes = iTunesApp()
252 if iTunes.player_state() == k.playing:
253 if amuaPlaying():
254 AmuaApp().display_album_details()
255 return
256 url = iTunes.current_stream_URL()
257 if url != k.missing_value:
258 if 'radioparadise.com' in url and 'review' not in url:
259 url = radioParadiseURL()
260 NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(url))
261 return
262 NSBeep()
263
264 def registerHotKey(self, func, keyCode, mods=0):
265 hotKeyRef = RegisterEventHotKey(keyCode, mods, (0, 0),
266 GetApplicationEventTarget(), 0)
267 self.hotKeys.append(hotKeyRef)
268 self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)] = func
269 return hotKeyRef
270
271 def unregisterHotKey(self, hotKeyRef):
272 self.hotKeys.remove(hotKeyRef)
273 del self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)]
274 hotKeyRef.UnregisterEventHotKey()
275
276 def incrementRatingBy(self, increment):
277 iTunes = iTunesApp()
278 if amuaPlaying():
279 if increment < 0:
280 AmuaApp().ban_song()
281 growlNotify('Banned song.', '', icon_of_application='Amua.app')
282 else:
283 AmuaApp().love_song()
284 growlNotify('Loved song.', '', icon_of_application='Amua.app')
285 return
286 rating = iTunes.current_track.rating()
287 rating += increment
288 if rating < 0:
289 rating = 0
290 NSBeep()
291 elif rating > 100:
292 rating = 100
293 NSBeep()
294 iTunes.current_track.rating.set(rating)
295
296 def playPause(self, useStereo=True):
297 global needsStereoPowerOn
298
299 iTunes = iTunesApp()
300 was_playing = (iTunes.player_state() == k.playing)
301 if not useStereo:
302 needsStereoPowerOn = False
303 if was_playing and amuaPlaying():
304 AmuaApp().stop()
305 else:
306 iTunes.playpause()
307 if not was_playing and iTunes.player_state() == k.stopped:
308 # most likely, we're focused on the iPod, so playing does nothing
309 iTunes.browser_windows[1].view.set(iTunes.user_playlists[its.name=='Stations'][1]())
310 iTunes.play()
311 if not useStereo:
312 return
313 if iTunes.player_state() == k.playing:
314 turnStereoOnOrOff()
315 else:
316 turnStereoOff()
317
318 def playPauseFront(self):
319 systemEvents = app(id='com.apple.systemEvents')
320 frontName = systemEvents.processes[its.frontmost == True][1].name()
321 if frontName == 'RealPlayer':
322 realPlayer = app(id='com.RealNetworks.RealPlayer')
323 if len(realPlayer.players()) > 0:
324 if realPlayer.players[1].state() == k.playing:
325 realPlayer.pause()
326 else:
327 realPlayer.play()
328 return
329 elif frontName == 'VLC':
330 app(id='org.videolan.vlc').play() # equivalent to playpause
331 else:
332 self.playPause(useStereo=False)
333
334 def nextTrack(self):
335 if amuaPlaying():
336 AmuaApp().skip_song()
337 return
338 iTunesApp().next_track()
339
340 def registerZoomWindowHotKey(self):
341 self.zoomWindowHotKey = self.registerHotKey(self.zoomWindow, 42, cmdKey | controlKey) # cmd-ctrl-\
342
343 def unregisterZoomWindowHotKey(self):
344 self.unregisterHotKey(self.zoomWindowHotKey)
345 self.zoomWindowHotKey = None
346
347 def zoomWindow(self):
348 # XXX detect if "enable access for assistive devices" needs to be enabled
349 systemEvents = app(id='com.apple.systemEvents')
350 frontName = systemEvents.processes[its.frontmost == True][1].name()
351 if frontName == 'iTunes':
352 systemEvents.processes['iTunes'].menu_bars[1]. \
353 menu_bar_items['Window'].menus.menu_items['Zoom'].click()
354 return
355 elif frontName in ('X11', 'XQuartz', 'Emacs'): # preserve C-M-\
356 self.unregisterZoomWindowHotKey()
357 systemEvents.key_code(42, using=[k.command_down, k.control_down])
358 self.registerZoomWindowHotKey()
359 return
360 frontPID = systemEvents.processes[its.frontmost == True][1].unix_id()
361 try:
362 zoomed = app(pid=frontPID).windows[1].zoomed
363 zoomed.set(not zoomed())
364 except (CommandError, RuntimeError):
365 systemEvents.processes[frontName].windows \
366 [its.subrole == 'AXStandardWindow'].windows[1]. \
367 buttons[its.subrole == 'AXZoomButton'].buttons[1].click()
368
369 def finishLaunching(self):
370 super(StreamVision, self).finishLaunching()
371
372 caches = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
373 NSUserDomainMask, True)[0]
374 cache = os.path.join(caches, 'StreamVision')
375 self.http = httplib2.Http(OneFileCache(cache), 5)
376 self.imagePath = os.path.join(cache, 'image')
377
378 self.registerHotKey(self.displayTrackInfo, 100) # F8
379 self.registerHotKey(self.goToSite, 100, cmdKey) # cmd-F8
380 self.registerHotKey(self.playPause, 101) # F9
381 self.registerHotKey(lambda: iTunesApp().previous_track(), 109) # F10
382 self.registerHotKey(self.nextTrack, 103) # F11
383 self.registerHotKey(lambda: self.incrementRatingBy(-20), 109, shiftKey) # shift-F10
384 self.registerHotKey(lambda: self.incrementRatingBy(20), 103, shiftKey) # shift-F11
385 self.registerZoomWindowHotKey()
386 NSDistributedNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, self.displayTrackInfo, 'com.apple.iTunes.playerInfo', None)
387 try:
388 import HIDRemote
389 HIDRemote.connect()
390 except ImportError:
391 print "failed to import HIDRemote (XXX fix - on Intel)"
392 except OSError, e:
393 print "failed to connect to remote: ", e
394
395 set_default_output_device_changed_callback(
396 self.defaultOutputDeviceChanged)
397 turnStereoOnOrOff()
398
399 def sendEvent_(self, theEvent):
400 eventType = theEvent.type()
401 if eventType == NSSystemDefined and \
402 theEvent.subtype() == kEventHotKeyPressedSubtype:
403 self.hotKeyActions[theEvent.data1()]()
404 elif eventType == NSApplicationDefined:
405 key = theEvent.data1()
406 if key == kHIDUsage_Csmr_ScanNextTrack:
407 self.nextTrack()
408 elif key == kHIDUsage_Csmr_ScanPreviousTrack:
409 iTunesApp().previous_track()
410 elif key == kHIDUsage_Csmr_PlayOrPause:
411 self.playPauseFront()
412 super(StreamVision, self).sendEvent_(theEvent)
413
414if __name__ == "__main__":
415 growlRegister()
416 AppHelper.runEventLoop()
417 try:
418 HIDRemote.disconnect()
419 except:
420 pass
Note: See TracBrowser for help on using the repository browser.