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
RevLine 
[188]1#!/usr/bin/pythonw
2# -*- coding: utf-8 -*-
3
[666]4from aem.ae import newdesc
[232]5from appscript import app, k, its, CommandError
[666]6from AppKit import (NSApplication, NSApplicationDefined, NSBeep, NSImage,
7 NSSystemDefined, NSURL, NSWorkspace)
8from Foundation import (NSDistributedNotificationCenter,
9 NSSearchPathForDirectoriesInDomains,
10 NSCachesDirectory, NSUserDomainMask)
[188]11from PyObjCTools import AppHelper
12from Carbon.CarbonEvt import RegisterEventHotKey, GetApplicationEventTarget
[232]13from Carbon.Events import cmdKey, shiftKey, controlKey
[666]14from AudioDevice import (default_output_device_is_airplay,
15 set_default_output_device_changed_callback)
[486]16import httplib2
17import os
[188]18import struct
19import scrape
[211]20import HotKey
[188]21
22GROWL_APP_NAME = 'StreamVision'
23NOTIFICATION_TRACK_INFO = 'iTunes Track Info'
24NOTIFICATIONS_ALL = [NOTIFICATION_TRACK_INFO]
25
26kEventHotKeyPressedSubtype = 6
27kEventHotKeyReleasedSubtype = 9
28
[300]29kHIDUsage_Csmr_ScanNextTrack = 0xB5
30kHIDUsage_Csmr_ScanPreviousTrack = 0xB6
31kHIDUsage_Csmr_PlayOrPause = 0xCD
32
[340]33def growlRegister():
34 global growl
35 growl = app(id='com.Growl.GrowlHelperApp')
[188]36
[340]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
[188]43
44def growlNotify(title, description, **kw):
[340]45 try:
[656]46 if usingStereo:
47 description += '\n(AirPlay)'
48
[666]49 if 'image' in kw:
50 image = (NSImage.alloc().initWithData_(buffer(kw['image']))
51 .TIFFRepresentation())
52 kw['image'] = newdesc('TIFF', image)
53
[340]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)
[195]63
[188]64def radioParadiseURL():
65 session = scrape.Session()
[645]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
[188]71
[194]72def cleanStreamTitle(title):
[316]73 if title == k.missing_value:
[194]74 return ''
75 title = title.split(' [')[0] # XXX move to description
[341]76 try: # incorrectly encoded?
77 title = title.encode('iso-8859-1').decode('utf-8')
[459]78 except (UnicodeDecodeError, UnicodeEncodeError):
[341]79 pass
[194]80 title = title.replace('`', u'’')
[323]81 return title
[195]82
[194]83def cleanStreamTrackName(name):
[188]84 name = name.split('. ')[0]
85 name = name.split(': ')[0]
[190]86 name = name.split(' - ')
87 if len(name) > 1:
88 name = ' - '.join(name[:-1])
89 else:
90 name = name[0]
[188]91 return name
[195]92
[663]93def iTunesApp(): return app(id='com.apple.iTunes')
[199]94def XTensionApp(): return app(creator='SHEx')
[414]95def AmuaApp(): return app('Amua.app')
[188]96
[199]97HAVE_XTENSION = False
98try:
99 XTensionApp()
100 HAVE_XTENSION = True
101except:
102 pass
103
[414]104HAVE_AMUA = False
105try:
106 AmuaApp()
107 HAVE_AMUA = True
108except:
109 pass
110
[324]111needsStereoPowerOn = HAVE_XTENSION
[656]112usingStereo = False
[324]113
[580]114def mayUseStereo():
115 if not HAVE_XTENSION:
116 return False
[663]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
[665]120 return any(d.kind() != k.computer
121 for d in iTunesApp().current_AirPlay_devices())
[663]122 except AttributeError:
123 pass
124
[580]125 systemEvents = app(id='com.apple.systemEvents')
[632]126 iTunesWindow = systemEvents.application_processes[u'iTunes'].windows[u'iTunes']
[646]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?
[658]131 return usingStereo
[638]132 return remote_speakers and remote_speakers[0] not in (None, k.missing_value)
[580]133
[656]134def turnStereoOnOrOff():
135 global needsStereoPowerOn, usingStereo
136 usingStereo = False
[655]137 if not default_output_device_is_airplay() and not mayUseStereo():
[631]138 if HAVE_XTENSION and XTensionApp().status('Stereo'):
139 XTensionApp().turnoff('Stereo')
[580]140 return
141 if not XTensionApp().status('Stereo'):
142 XTensionApp().turnon('Stereo')
[656]143 usingStereo = True
[580]144 needsStereoPowerOn = False
145
146def turnStereoOff():
[656]147 global needsStereoPowerOn, usingStereo
148 usingStereo = False
[653]149 if default_output_device_is_airplay() or not mayUseStereo():
[580]150 return
151 if not needsStereoPowerOn and XTensionApp().status('Stereo'):
152 XTensionApp().turnoff('Stereo')
153 needsStereoPowerOn = True
154
[414]155def amuaPlaying():
156 if not HAVE_AMUA:
157 return False
158 return AmuaApp().is_playing()
159
[486]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
[188]182class StreamVision(NSApplication):
183
184 hotKeyActions = {}
185 hotKeys = []
186
[414]187 def displayTrackInfo(self, playerInfo=None):
[188]188 iTunes = iTunesApp()
[195]189
[497]190 try:
191 trackClass = iTunes.current_track.class_()
192 except CommandError:
193 trackClass = k.property
194
[211]195 trackName = ''
[315]196 if trackClass != k.property:
197 trackName = iTunes.current_track.name()
[211]198
[647]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)
[580]206 turnStereoOff()
[211]207 return
[656]208 turnStereoOnOrOff()
[211]209 if trackClass == k.URL_track:
[414]210 if amuaPlaying():
211 if playerInfo is None: # Amua displays it itself
212 AmuaApp().display_song_information()
213 return
[486]214 url = iTunes.current_stream_URL()
215 kw = {}
216 if url != k.missing_value and url.endswith('.jpg'):
[650]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/'):
[666]224 kw['image'] = content
[315]225 growlNotify(cleanStreamTitle(iTunes.current_stream_title()),
[486]226 cleanStreamTrackName(trackName), **kw)
[211]227 return
[315]228 if trackClass == k.property:
[324]229 growlNotify('iTunes is playing.', '')
230 return
[211]231 kw = {}
232 # XXX iTunes doesn't let you get artwork for shared tracks
233 if trackClass != k.shared_track:
[315]234 artwork = iTunes.current_track.artworks()
[211]235 if artwork:
[487]236 try:
[666]237 kw['image'] = artwork[0].data_().data
[487]238 except CommandError:
239 pass
[211]240 growlNotify(trackName + ' ' +
[323]241 u'★' * (iTunes.current_track.rating() / 20),
242 iTunes.current_track.album() + '\n' +
[315]243 iTunes.current_track.artist(),
[211]244 **kw)
245
[656]246 def defaultOutputDeviceChanged(self):
247 turnStereoOnOrOff()
248 self.displayTrackInfo()
249
[188]250 def goToSite(self):
251 iTunes = iTunesApp()
[315]252 if iTunes.player_state() == k.playing:
[414]253 if amuaPlaying():
254 AmuaApp().display_album_details()
255 return
[315]256 url = iTunes.current_stream_URL()
[316]257 if url != k.missing_value:
[252]258 if 'radioparadise.com' in url and 'review' not in url:
[188]259 url = radioParadiseURL()
260 NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(url))
261 return
262 NSBeep()
[195]263
[188]264 def registerHotKey(self, func, keyCode, mods=0):
265 hotKeyRef = RegisterEventHotKey(keyCode, mods, (0, 0),
266 GetApplicationEventTarget(), 0)
267 self.hotKeys.append(hotKeyRef)
[211]268 self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)] = func
[235]269 return hotKeyRef
[188]270
[235]271 def unregisterHotKey(self, hotKeyRef):
272 self.hotKeys.remove(hotKeyRef)
273 del self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)]
274 hotKeyRef.UnregisterEventHotKey()
275
[199]276 def incrementRatingBy(self, increment):
277 iTunes = iTunesApp()
[414]278 if amuaPlaying():
[415]279 if increment < 0:
[414]280 AmuaApp().ban_song()
[458]281 growlNotify('Banned song.', '', icon_of_application='Amua.app')
[414]282 else:
283 AmuaApp().love_song()
[458]284 growlNotify('Loved song.', '', icon_of_application='Amua.app')
[414]285 return
[315]286 rating = iTunes.current_track.rating()
[199]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
[300]296 def playPause(self, useStereo=True):
[324]297 global needsStereoPowerOn
298
[199]299 iTunes = iTunesApp()
[315]300 was_playing = (iTunes.player_state() == k.playing)
[324]301 if not useStereo:
302 needsStereoPowerOn = False
[414]303 if was_playing and amuaPlaying():
304 AmuaApp().stop()
305 else:
306 iTunes.playpause()
[315]307 if not was_playing and iTunes.player_state() == k.stopped:
[234]308 # most likely, we're focused on the iPod, so playing does nothing
[315]309 iTunes.browser_windows[1].view.set(iTunes.user_playlists[its.name=='Stations'][1]())
[234]310 iTunes.play()
[580]311 if not useStereo:
312 return
313 if iTunes.player_state() == k.playing:
[656]314 turnStereoOnOrOff()
[580]315 else:
316 turnStereoOff()
[235]317
[301]318 def playPauseFront(self):
319 systemEvents = app(id='com.apple.systemEvents')
[340]320 frontName = systemEvents.processes[its.frontmost == True][1].name()
[301]321 if frontName == 'RealPlayer':
322 realPlayer = app(id='com.RealNetworks.RealPlayer')
[315]323 if len(realPlayer.players()) > 0:
324 if realPlayer.players[1].state() == k.playing:
325 realPlayer.pause()
326 else:
327 realPlayer.play()
328 return
[301]329 elif frontName == 'VLC':
330 app(id='org.videolan.vlc').play() # equivalent to playpause
331 else:
[302]332 self.playPause(useStereo=False)
[301]333
[414]334 def nextTrack(self):
335 if amuaPlaying():
336 AmuaApp().skip_song()
337 return
338 iTunesApp().next_track()
339
[235]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
[232]347 def zoomWindow(self):
[313]348 # XXX detect if "enable access for assistive devices" needs to be enabled
[232]349 systemEvents = app(id='com.apple.systemEvents')
[340]350 frontName = systemEvents.processes[its.frontmost == True][1].name()
[233]351 if frontName == 'iTunes':
[232]352 systemEvents.processes['iTunes'].menu_bars[1]. \
353 menu_bar_items['Window'].menus.menu_items['Zoom'].click()
354 return
[631]355 elif frontName in ('X11', 'XQuartz', 'Emacs'): # preserve C-M-\
[235]356 self.unregisterZoomWindowHotKey()
357 systemEvents.key_code(42, using=[k.command_down, k.control_down])
358 self.registerZoomWindowHotKey()
359 return
[340]360 frontPID = systemEvents.processes[its.frontmost == True][1].unix_id()
[232]361 try:
[339]362 zoomed = app(pid=frontPID).windows[1].zoomed
[232]363 zoomed.set(not zoomed())
[235]364 except (CommandError, RuntimeError):
[313]365 systemEvents.processes[frontName].windows \
366 [its.subrole == 'AXStandardWindow'].windows[1]. \
367 buttons[its.subrole == 'AXZoomButton'].buttons[1].click()
[199]368
[188]369 def finishLaunching(self):
370 super(StreamVision, self).finishLaunching()
[486]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
[188]378 self.registerHotKey(self.displayTrackInfo, 100) # F8
379 self.registerHotKey(self.goToSite, 100, cmdKey) # cmd-F8
[199]380 self.registerHotKey(self.playPause, 101) # F9
[188]381 self.registerHotKey(lambda: iTunesApp().previous_track(), 109) # F10
[414]382 self.registerHotKey(self.nextTrack, 103) # F11
[211]383 self.registerHotKey(lambda: self.incrementRatingBy(-20), 109, shiftKey) # shift-F10
384 self.registerHotKey(lambda: self.incrementRatingBy(20), 103, shiftKey) # shift-F11
[235]385 self.registerZoomWindowHotKey()
[193]386 NSDistributedNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, self.displayTrackInfo, 'com.apple.iTunes.playerInfo', None)
[300]387 try:
[302]388 import HIDRemote
[300]389 HIDRemote.connect()
[302]390 except ImportError:
391 print "failed to import HIDRemote (XXX fix - on Intel)"
[300]392 except OSError, e:
393 print "failed to connect to remote: ", e
[188]394
[656]395 set_default_output_device_changed_callback(
396 self.defaultOutputDeviceChanged)
397 turnStereoOnOrOff()
[654]398
[188]399 def sendEvent_(self, theEvent):
[300]400 eventType = theEvent.type()
401 if eventType == NSSystemDefined and \
[188]402 theEvent.subtype() == kEventHotKeyPressedSubtype:
403 self.hotKeyActions[theEvent.data1()]()
[300]404 elif eventType == NSApplicationDefined:
405 key = theEvent.data1()
406 if key == kHIDUsage_Csmr_ScanNextTrack:
[415]407 self.nextTrack()
[300]408 elif key == kHIDUsage_Csmr_ScanPreviousTrack:
409 iTunesApp().previous_track()
410 elif key == kHIDUsage_Csmr_PlayOrPause:
[301]411 self.playPauseFront()
[188]412 super(StreamVision, self).sendEvent_(theEvent)
413
414if __name__ == "__main__":
[340]415 growlRegister()
[195]416 AppHelper.runEventLoop()
[339]417 try:
418 HIDRemote.disconnect()
419 except:
420 pass
Note: See TracBrowser for help on using the repository browser.