source: trunk/StreamVision/StreamVision.py@ 596

Last change on this file since 596 was 580, checked in by Nicholas Riley, 15 years ago

StreamVision.py: Only turn on/off stereo if iTunes is transmitting to
AirPort Express.

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